From 9c084e79bba65984b24b86d8259f3a93c4fcba52 Mon Sep 17 00:00:00 2001 From: Pallab Maiti Date: Sun, 4 Feb 2024 16:57:23 +0530 Subject: [PATCH] feat: support for multiple instance --- .gitignore | 5 +- .../RudderConfig/SampleRudderConfig.plist | 4 + .../SampleSwift-iOS.xcodeproj/project.pbxproj | 12 +- .../SampleSwift-iOS/AppDelegate.swift | 66 ++- .../SampleSwift-iOS/ViewController.swift | 71 +-- Rudder.xcodeproj/project.pbxproj | 354 +++++++++-- RudderTests/Core/ClientTests.swift | 191 +++--- RudderTests/Core/DataResidencyTests.swift | 56 +- .../DataUpload/DataUploadWorkerTests.swift | 14 +- .../Core/Logger/ConsoleLoggerTests.swift | 106 ++++ RudderTests/Core/Logger/LoggerTests.swift | 137 +++++ RudderTests/Core/MultipleInstanceTests.swift | 138 +++++ .../Networking/URLSessionClientTests.swift | 4 +- .../Core/Plugins/EventFilteringTests.swift | 139 +++++ .../Core/Plugins/ReplayQueuePluginTests.swift | 305 ++++++++++ .../Core/Plugins/UserSessionPluginTests.swift | 311 ++++++++++ .../SourceConfigDownloadTests.swift | 4 +- .../SourceConfigDownloadWorkerTests.swift | 4 +- .../Core/Storage/SQLiteStorageTests.swift | 118 ++++ .../Core/Storage/StorageMigratorTests.swift | 60 ++ ...geTests.swift => StorageWorkerTests.swift} | 16 +- RudderTests/Mocks/APIClientMock.swift | 2 +- RudderTests/Mocks/ConsoleLoggerMock.swift | 27 + RudderTests/Mocks/CoreMocks.swift | 75 ++- RudderTests/Mocks/NOLogger.swift | 20 +- RudderTests/Mocks/PrintFunctionMock.swift | 24 + RudderTests/Mocks/RSClientMock.swift | 180 ++++++ RudderTests/Mocks/SQLiteDatabaseMock.swift | 75 +++ RudderTests/Mocks/SessionStorageMock.swift | 22 + RudderTests/Mocks/SourceConfigMock.swift | 2 +- RudderTests/Mocks/StorageMock.swift | 46 +- .../MacApplicationState.swift | 6 +- .../PhoneApplicationState.swift | 6 +- .../WatchApplicationState.swift | 6 +- Sources/Classes/Core/ClientRegistry.swift | 48 ++ .../Core/Common/Constants/LogMessages.swift | 49 +- .../Core/DataUpload/DataUploadWorker.swift | 27 +- .../{Config.swift => Configuration.swift} | 82 ++- .../Classes/Core/Domain/Models/Context.swift | 4 +- .../Core/Domain/Models/StorageMessage.swift | 4 +- Sources/Classes/Core/Helpers/Logger.swift | 117 ---- .../Classes/Core/Helpers/SessionStorage.swift | 29 +- .../Core/Helpers/UserDefaultsWorker.swift | 6 +- Sources/Classes/Core/Helpers/Utility.swift | 17 - .../Classes/Core/Logger/ConsoleLogger.swift | 36 ++ Sources/Classes/Core/Logger/LogLevel.swift | 35 ++ Sources/Classes/Core/Logger/Logger.swift | 51 ++ Sources/Classes/Core/Message.swift | 4 +- Sources/Classes/Core/ObjC/ObjCRSClient.swift | 81 +++ .../Classes/Core/Plugins/ContextPlugin.swift | 16 +- .../Classes/Core/Plugins/EventFiltering.swift | 4 +- .../Core/Plugins/IntegrationPlugin.swift | 4 +- Sources/Classes/Core/Plugins/Plugins.swift | 2 +- .../Core/Plugins/ReplayQueuePlugin.swift | 69 ++- .../Classes/Core/Plugins/StoragePlugin.swift | 10 +- .../Core/Plugins/UserSessionPlugin.swift | 204 ++++--- .../Classes/Core/Policies/FlushPolicy.swift | 4 +- Sources/Classes/Core/RSClient.swift | 559 +++++++++--------- .../{Controller.swift => RSClientCore.swift} | 281 +++++---- Sources/Classes/Core/RSClientProtocol.swift | 105 ++++ .../Model/Destination.swift | 20 +- .../SourceConfigDownload.swift | 6 +- .../SourceConfigDownloadWorker.swift | 35 +- .../Storage/{ => Database}/Database.swift | 13 + .../SQLiteDatabase.swift} | 18 +- .../Storage/Migration/StorageMigration.swift | 21 + .../Storage/Migration/StorageMigrator.swift | 40 ++ ...faultStorage.swift => SQLiteStorage.swift} | 23 +- Sources/Classes/Core/Storage/Storage.swift | 6 +- .../Classes/Core/Storage/StorageWorker.swift | 11 +- 70 files changed, 3553 insertions(+), 1094 deletions(-) create mode 100644 RudderTests/Core/Logger/ConsoleLoggerTests.swift create mode 100644 RudderTests/Core/Logger/LoggerTests.swift create mode 100644 RudderTests/Core/MultipleInstanceTests.swift create mode 100644 RudderTests/Core/Plugins/EventFilteringTests.swift create mode 100644 RudderTests/Core/Plugins/ReplayQueuePluginTests.swift create mode 100644 RudderTests/Core/Plugins/UserSessionPluginTests.swift create mode 100644 RudderTests/Core/Storage/SQLiteStorageTests.swift create mode 100644 RudderTests/Core/Storage/StorageMigratorTests.swift rename RudderTests/Core/Storage/{StorageTests.swift => StorageWorkerTests.swift} (85%) create mode 100644 RudderTests/Mocks/ConsoleLoggerMock.swift create mode 100644 RudderTests/Mocks/PrintFunctionMock.swift create mode 100644 RudderTests/Mocks/RSClientMock.swift create mode 100644 RudderTests/Mocks/SQLiteDatabaseMock.swift create mode 100644 RudderTests/Mocks/SessionStorageMock.swift create mode 100644 Sources/Classes/Core/ClientRegistry.swift rename Sources/Classes/Core/Domain/Models/{Config.swift => Configuration.swift} (73%) delete mode 100644 Sources/Classes/Core/Helpers/Logger.swift create mode 100644 Sources/Classes/Core/Logger/ConsoleLogger.swift create mode 100644 Sources/Classes/Core/Logger/LogLevel.swift create mode 100644 Sources/Classes/Core/Logger/Logger.swift create mode 100644 Sources/Classes/Core/ObjC/ObjCRSClient.swift rename Sources/Classes/Core/{Controller.swift => RSClientCore.swift} (63%) create mode 100644 Sources/Classes/Core/RSClientProtocol.swift rename Sources/Classes/Core/Storage/{ => Database}/Database.swift (85%) rename Sources/Classes/Core/Storage/{DefaultDatabase.swift => Database/SQLiteDatabase.swift} (72%) create mode 100644 Sources/Classes/Core/Storage/Migration/StorageMigration.swift create mode 100644 Sources/Classes/Core/Storage/Migration/StorageMigrator.swift rename Sources/Classes/Core/Storage/{DefaultStorage.swift => SQLiteStorage.swift} (90%) diff --git a/.gitignore b/.gitignore index 8746c6ff..cc5a8297 100644 --- a/.gitignore +++ b/.gitignore @@ -95,5 +95,6 @@ iOSInjectionProject/ relative_or_absolute_path_to_cache_location compile_commands.json GoogleService-Info.plist -RudderConfig.plist -configuration.json \ No newline at end of file +RudderConfig*.plist +configuration.json +RudderTests/**/*.plist \ No newline at end of file diff --git a/Examples/RudderConfig/SampleRudderConfig.plist b/Examples/RudderConfig/SampleRudderConfig.plist index 4e130dde..9205053d 100644 --- a/Examples/RudderConfig/SampleRudderConfig.plist +++ b/Examples/RudderConfig/SampleRudderConfig.plist @@ -2,6 +2,10 @@ + MOCK_DATA_PLANE_URL + + MOCK_CONTROL_PLANE_URL + WRITE_KEY PROD_DATA_PLANE_URL diff --git a/Examples/SampleSwift-iOS/SampleSwift-iOS.xcodeproj/project.pbxproj b/Examples/SampleSwift-iOS/SampleSwift-iOS.xcodeproj/project.pbxproj index df91196f..f379c816 100644 --- a/Examples/SampleSwift-iOS/SampleSwift-iOS.xcodeproj/project.pbxproj +++ b/Examples/SampleSwift-iOS/SampleSwift-iOS.xcodeproj/project.pbxproj @@ -14,12 +14,13 @@ 06EABC8F24665E480043D720 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 06EABC8E24665E480043D720 /* Assets.xcassets */; }; 06EABC9224665E480043D720 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 06EABC9024665E480043D720 /* LaunchScreen.storyboard */; }; 1BBDBFA2CA1F65262723A637 /* Pods_SampleSwift_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72016B33854F923C48E9084A /* Pods_SampleSwift_iOS.framework */; }; - ED333EC62B2358B4003EB0B3 /* RudderConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = ED333EC32B2358B4003EB0B3 /* RudderConfig.plist */; }; + ED333EC62B2358B4003EB0B3 /* RudderConfig_1.plist in Resources */ = {isa = PBXBuildFile; fileRef = ED333EC32B2358B4003EB0B3 /* RudderConfig_1.plist */; }; ED333EC72B2358B4003EB0B3 /* SampleRudderConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = ED333EC42B2358B4003EB0B3 /* SampleRudderConfig.plist */; }; ED333EC82B2358B4003EB0B3 /* RudderConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED333EC52B2358B4003EB0B3 /* RudderConfig.swift */; }; ED333ECA2B235D8E003EB0B3 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = ED333EC92B235D8E003EB0B3 /* GoogleService-Info.plist */; }; ED4EACA127F5718100207AF1 /* RSCustomDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED4EACA027F5718100207AF1 /* RSCustomDestination.swift */; }; EDC1BBC82B0B2E7D00211F24 /* Configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = EDC1BBC72B0B2E7D00211F24 /* Configuration.json */; }; + EDEDC1382B6A336D0082C7E2 /* RudderConfig_2.plist in Resources */ = {isa = PBXBuildFile; fileRef = EDEDC1372B6A336D0082C7E2 /* RudderConfig_2.plist */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -34,12 +35,13 @@ 6AF9E4CE2308F413BD375A80 /* Pods-SampleSwift-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleSwift-iOS.debug.xcconfig"; path = "Target Support Files/Pods-SampleSwift-iOS/Pods-SampleSwift-iOS.debug.xcconfig"; sourceTree = ""; }; 72016B33854F923C48E9084A /* Pods_SampleSwift_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SampleSwift_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 80FF9E334811B6015D51C152 /* Pods-SampleSwift-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleSwift-iOS.release.xcconfig"; path = "Target Support Files/Pods-SampleSwift-iOS/Pods-SampleSwift-iOS.release.xcconfig"; sourceTree = ""; }; - ED333EC32B2358B4003EB0B3 /* RudderConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = RudderConfig.plist; sourceTree = ""; }; + ED333EC32B2358B4003EB0B3 /* RudderConfig_1.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = RudderConfig_1.plist; sourceTree = ""; }; ED333EC42B2358B4003EB0B3 /* SampleRudderConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = SampleRudderConfig.plist; sourceTree = ""; }; ED333EC52B2358B4003EB0B3 /* RudderConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RudderConfig.swift; sourceTree = ""; }; ED333EC92B235D8E003EB0B3 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; ED4EACA027F5718100207AF1 /* RSCustomDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSCustomDestination.swift; sourceTree = ""; }; EDC1BBC72B0B2E7D00211F24 /* Configuration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Configuration.json; sourceTree = ""; }; + EDEDC1372B6A336D0082C7E2 /* RudderConfig_2.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = RudderConfig_2.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -111,7 +113,8 @@ ED333EC22B2358B4003EB0B3 /* RudderConfig */ = { isa = PBXGroup; children = ( - ED333EC32B2358B4003EB0B3 /* RudderConfig.plist */, + ED333EC32B2358B4003EB0B3 /* RudderConfig_1.plist */, + EDEDC1372B6A336D0082C7E2 /* RudderConfig_2.plist */, ED333EC52B2358B4003EB0B3 /* RudderConfig.swift */, ED333EC42B2358B4003EB0B3 /* SampleRudderConfig.plist */, ); @@ -182,7 +185,8 @@ ED333EC72B2358B4003EB0B3 /* SampleRudderConfig.plist in Resources */, 06EABC9224665E480043D720 /* LaunchScreen.storyboard in Resources */, 06EABC8F24665E480043D720 /* Assets.xcassets in Resources */, - ED333EC62B2358B4003EB0B3 /* RudderConfig.plist in Resources */, + EDEDC1382B6A336D0082C7E2 /* RudderConfig_2.plist in Resources */, + ED333EC62B2358B4003EB0B3 /* RudderConfig_1.plist in Resources */, EDC1BBC82B0B2E7D00211F24 /* Configuration.json in Resources */, 06EABC8D24665E470043D720 /* Main.storyboard in Resources */, ED333ECA2B235D8E003EB0B3 /* GoogleService-Info.plist in Resources */, diff --git a/Examples/SampleSwift-iOS/SampleSwift-iOS/AppDelegate.swift b/Examples/SampleSwift-iOS/SampleSwift-iOS/AppDelegate.swift index 6aad16e0..8d8c1ce8 100644 --- a/Examples/SampleSwift-iOS/SampleSwift-iOS/AppDelegate.swift +++ b/Examples/SampleSwift-iOS/SampleSwift-iOS/AppDelegate.swift @@ -14,32 +14,58 @@ import Network @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var client: RSClient! + var client1: RSClient! + var client2: RSClient! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - guard let path = Bundle.main.path(forResource: "RudderConfig", ofType: "plist"), - let data = try? Data(contentsOf: URL(fileURLWithPath: path)), - let rudderConfig = try? PropertyListDecoder().decode(RudderConfig.self, from: data) else { - return true - } - print(NSHomeDirectory()) - if let config: Config = Config(writeKey: rudderConfig.WRITE_KEY, dataPlaneURL: rudderConfig.DEV_DATA_PLANE_URL)? - .controlPlaneURL(rudderConfig.DEV_CONTROL_PLANE_URL) -// .dataResidencyServer(.EU) -// .controlPlaneURL("https://e2e6fd4f-c24c-43d6-8ca3-11a11e7cc7d5.mock.pstmn.io") // disabled -// .controlPlaneURL("https://98e2b8de-9984-471b-a705-b1bcf3f9f6ba.mock.pstmn.io") // enabled - .loglevel(.verbose) - .trackLifecycleEvents(true) - .recordScreenViews(true) -// .sleepTimeOut(10) - .gzipEnabled(false) { - - client = RSClient(config: config) + if let path = Bundle.main.path(forResource: "RudderConfig_1", ofType: "plist"), + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let rudderConfig = try? PropertyListDecoder().decode(RudderConfig.self, from: data) + { + if let config: Configuration = Configuration( + writeKey: rudderConfig.WRITE_KEY, + dataPlaneURL: rudderConfig.DEV_DATA_PLANE_URL + )? + .controlPlaneURL(rudderConfig.DEV_CONTROL_PLANE_URL) + // .controlPlaneURL("https://e2e6fd4f-c24c-43d6-8ca3-11a11e7cc7d5.mock.pstmn.io") // disabled + // .controlPlaneURL("https://98e2b8de-9984-471b-a705-b1bcf3f9f6ba.mock.pstmn.io") // enabled + .logLevel(.verbose) + .trackLifecycleEvents(true) + .recordScreenViews(true) + .sleepTimeOut(5) + .gzipEnabled(false) + .flushQueueSize(0) + { + client1 = RSClient.initialize(with: config) + } + } + + if let path = Bundle.main.path(forResource: "RudderConfig_2", ofType: "plist"), + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let rudderConfig = try? PropertyListDecoder().decode(RudderConfig.self, from: data) + { + if let config: Configuration = Configuration( + writeKey: rudderConfig.WRITE_KEY, + dataPlaneURL: rudderConfig.DEV_DATA_PLANE_URL + )? + .controlPlaneURL(rudderConfig.DEV_CONTROL_PLANE_URL) + // .controlPlaneURL("https://e2e6fd4f-c24c-43d6-8ca3-11a11e7cc7d5.mock.pstmn.io") // disabled + // .controlPlaneURL("https://98e2b8de-9984-471b-a705-b1bcf3f9f6ba.mock.pstmn.io") // enabled + .logLevel(.verbose) + .trackLifecycleEvents(false) + .recordScreenViews(false) + .sleepTimeOut(5) + .gzipEnabled(false) + .flushQueueSize(0) + { + client2 = RSClient.initialize(with: config, instanceName: "") + } } - + + return true } diff --git a/Examples/SampleSwift-iOS/SampleSwift-iOS/ViewController.swift b/Examples/SampleSwift-iOS/SampleSwift-iOS/ViewController.swift index 8b6aada6..151ccb6d 100644 --- a/Examples/SampleSwift-iOS/SampleSwift-iOS/ViewController.swift +++ b/Examples/SampleSwift-iOS/SampleSwift-iOS/ViewController.swift @@ -21,7 +21,8 @@ struct Task { class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! - var client: RSClient! + var client1: RSClient! + var client2: RSClient! let taskList: [Task] = [ Task(name: "Identify with traits"), @@ -67,7 +68,8 @@ class ViewController: UIViewController { tableView.dataSource = self tableView.delegate = self - client = (UIApplication.shared.delegate as? AppDelegate)!.client + client1 = (UIApplication.shared.delegate as? AppDelegate)!.client1 + client2 = (UIApplication.shared.delegate as? AppDelegate)!.client2 } } @@ -87,7 +89,7 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch indexPath.row { case 0: - client.identify("test_user_id", traits: [ + client1.identify("test_user_id", traits: [ "integerValue": 42, "stringValue": "Hello, World!", "boolValue": true, @@ -98,10 +100,10 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { "dateValue": Date() ]) case 1: - client.identify("test_user_id") + client1.identify("test_user_id") case 2: - client.track("single_track_call", properties: [ + client1.track("single_track_call", properties: [ "integerValue": 42, "stringValue": "Hello, World!", "boolValue": true, @@ -112,37 +114,38 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { "dateValue": Date() ]) case 3: - client.track("single_track_call") + client1.track("single_track_call") + client2.track("single_track_call") case 4: - client.alias("new_user_id") + client1.alias("new_user_id") case 5: - client.screen("ViewController", properties: ["key_1": "value_1"]) + client1.screen("ViewController", properties: ["key_1": "value_1"]) case 6: - client.screen("ViewController") + client1.screen("ViewController") case 7: - client.group("test_group_id", traits: ["key_1": "value_1"]) + client1.group("test_group_id", traits: ["key_1": "value_1"]) case 8: - client.group("test_group_id") + client1.group("test_group_id") case 9: - client.setAnonymousId("anonymous_id_1") + client1.setAnonymousId("anonymous_id_1") case 10: - client.setDeviceToken("device_token_1") + client1.setDeviceToken("device_token_1") case 11: - client.setAdvertisingId("advertising_id_1") + client1.setAdvertisingId("advertising_id_1") case 12: - client.setOptOutStatus(false) + client1.setOptOutStatus(false) case 13: - client.setOptOutStatus(true) + client1.setOptOutStatus(true) case 14: - client.reset(and: false) + client1.reset(and: false) case 15: - client.flush() + client1.flush() case 16: - client.setAnonymousId("anonymous_id_2") + client1.setAnonymousId("anonymous_id_2") case 17: - client.setDeviceToken("device_token_2") + client1.setDeviceToken("device_token_2") case 18: - client.setAdvertisingId("advertising_id_2") + client1.setAdvertisingId("advertising_id_2") case 19: let option = Option() @@ -151,7 +154,7 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { option.putIntegration("key-7", isEnabled: true) option.putIntegration("key-8", isEnabled: false) - client.setOption(option) + client1.setOption(option) case 20: let option = IdentifyOption() option.putExternalId("value-1", to: "key-1") @@ -166,9 +169,11 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { option.putCustomContext(["Key-02": "value-1"], for: "key-10") option.putCustomContext(["Key-03": "value-1"], for: "key-11") option.putCustomContext(["Key-04": "value-1"], for: "key-12") - client.identify("test_user_id", option: option) + client1.identify("test_user_id", option: option) case 21: let option = MessageOption() + .putCustomContext([:], for: "") + .putIntegration("", isEnabled: true) option.putIntegration("key-5", isEnabled: false) option.putIntegration("key-6", isEnabled: true) @@ -177,19 +182,19 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { option.putCustomContext(["Key-01": "value-1"], for: "key-9") option.putCustomContext(["Key-02": "value-2"], for: "key-10") - client.track("single_track_call", option: option) + client1.track("single_track_call", option: option) case 22: - client.startSession() + client1.startSession() case 23: - client.endSession() + client1.endSession() case 24: - client.startSession(1234567890) + client1.startSession(1234567890) case 25: - if let context = client.context, let dictionaryValue = context.dictionaryValue { + if let context = client1.context, let dictionaryValue = context.dictionaryValue { print(dictionaryValue) } case 26: - client.track("allow_list_track", properties: [ + client1.track("allow_list_track", properties: [ "integerValue": 42, "stringValue": "Hello, World!", "boolValue": true, @@ -200,7 +205,7 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { "dateValue": Date() ]) case 27: - client.track("deny_list_track", properties: [ + client1.track("deny_list_track", properties: [ "integerValue": 42, "stringValue": "Hello, World!", "boolValue": true, @@ -211,12 +216,12 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { "dateValue": Date() ]) case 28: - client.track("Order Done", properties: getProperties()) + client1.track("Order Done", properties: getProperties()) case 29: - client.track("Order Completed", properties: getProperties()) + client1.track("Order Completed", properties: getProperties()) case 30: for i in 1...50 { - client.track("Track \(i)", properties: ["time": Date().timeIntervalSince1970]) + client1.track("Track \(i)", properties: ["time": Date().timeIntervalSince1970]) } /* case 4: diff --git a/Rudder.xcodeproj/project.pbxproj b/Rudder.xcodeproj/project.pbxproj index 2b5f4079..251bf449 100644 --- a/Rudder.xcodeproj/project.pbxproj +++ b/Rudder.xcodeproj/project.pbxproj @@ -168,22 +168,10 @@ ED8ED1BB2B51486E00DAE613 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1B92B51486E00DAE613 /* Storage.swift */; }; ED8ED1BC2B51486E00DAE613 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1B92B51486E00DAE613 /* Storage.swift */; }; ED8ED1BD2B51486E00DAE613 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1B92B51486E00DAE613 /* Storage.swift */; }; - ED8ED1BF2B514B4C00DAE613 /* DefaultDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1BE2B514B4B00DAE613 /* DefaultDatabase.swift */; }; - ED8ED1C02B514B4C00DAE613 /* DefaultDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1BE2B514B4B00DAE613 /* DefaultDatabase.swift */; }; - ED8ED1C12B514B4C00DAE613 /* DefaultDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1BE2B514B4B00DAE613 /* DefaultDatabase.swift */; }; - ED8ED1C22B514B4C00DAE613 /* DefaultDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1BE2B514B4B00DAE613 /* DefaultDatabase.swift */; }; - ED8ED1C42B514B6A00DAE613 /* DefaultStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1C32B514B6A00DAE613 /* DefaultStorage.swift */; }; - ED8ED1C52B514B6A00DAE613 /* DefaultStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1C32B514B6A00DAE613 /* DefaultStorage.swift */; }; - ED8ED1C62B514B6A00DAE613 /* DefaultStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1C32B514B6A00DAE613 /* DefaultStorage.swift */; }; - ED8ED1C72B514B6A00DAE613 /* DefaultStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1C32B514B6A00DAE613 /* DefaultStorage.swift */; }; ED8ED1C92B514B8D00DAE613 /* StorageWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1C82B514B8C00DAE613 /* StorageWorker.swift */; }; ED8ED1CA2B514B8D00DAE613 /* StorageWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1C82B514B8C00DAE613 /* StorageWorker.swift */; }; ED8ED1CB2B514B8D00DAE613 /* StorageWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1C82B514B8C00DAE613 /* StorageWorker.swift */; }; ED8ED1CC2B514B8D00DAE613 /* StorageWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1C82B514B8C00DAE613 /* StorageWorker.swift */; }; - ED8ED1D92B53B9F600DAE613 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1D82B53B9F600DAE613 /* Controller.swift */; }; - ED8ED1DA2B53B9F600DAE613 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1D82B53B9F600DAE613 /* Controller.swift */; }; - ED8ED1DB2B53B9F600DAE613 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1D82B53B9F600DAE613 /* Controller.swift */; }; - ED8ED1DC2B53B9F600DAE613 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1D82B53B9F600DAE613 /* Controller.swift */; }; ED8ED1DE2B55361900DAE613 /* StoragePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1DD2B55361900DAE613 /* StoragePlugin.swift */; }; ED8ED1DF2B55361900DAE613 /* StoragePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1DD2B55361900DAE613 /* StoragePlugin.swift */; }; ED8ED1E02B55361900DAE613 /* StoragePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED1DD2B55361900DAE613 /* StoragePlugin.swift */; }; @@ -280,7 +268,6 @@ EDA7EF842739119600E73142 /* UserDefaultsWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF452739119600E73142 /* UserDefaultsWorker.swift */; }; EDA7EF862739119600E73142 /* String+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF482739119600E73142 /* String+Ext.swift */; }; EDA7EF8B2739119600E73142 /* Rudder.h in Headers */ = {isa = PBXBuildFile; fileRef = EDA7EF4F2739119600E73142 /* Rudder.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EDA7EF922739119600E73142 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF592739119600E73142 /* Config.swift */; }; EDC1327C27D614C400AFD833 /* ClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC1327B27D614C400AFD833 /* ClientTests.swift */; }; EDC132A227D7D61D00AFD833 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC132A127D7D61D00AFD833 /* JSON.swift */; }; EDC1BBCB2B0DD87D00211F24 /* Rudder.h in Headers */ = {isa = PBXBuildFile; fileRef = EDA7EF4F2739119600E73142 /* Rudder.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -290,7 +277,6 @@ EDC1BBDD2B0DD87D00211F24 /* ReplayQueuePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3D227A27A3F0C100EC8366 /* ReplayQueuePlugin.swift */; }; EDC1BBDF2B0DD87D00211F24 /* EventsAndKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED277FA027421B06002A8FEA /* EventsAndKeys.swift */; }; EDC1BBE02B0DD87D00211F24 /* IntegrationPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED157C4427CFD99700F22202 /* IntegrationPlugin.swift */; }; - EDC1BBE42B0DD87D00211F24 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF592739119600E73142 /* Config.swift */; }; EDC1BBE62B0DD87D00211F24 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF332739119600E73142 /* API.swift */; }; EDC1BBE82B0DD87D00211F24 /* RSVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED05631E291BEB0400BAEE65 /* RSVersion.swift */; }; EDC1BBF32B0DD87D00211F24 /* TypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3D22BD27A45B6500EC8366 /* TypeAlias.swift */; }; @@ -307,7 +293,6 @@ EDC1BC222B0DD88A00211F24 /* ReplayQueuePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3D227A27A3F0C100EC8366 /* ReplayQueuePlugin.swift */; }; EDC1BC242B0DD88A00211F24 /* EventsAndKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED277FA027421B06002A8FEA /* EventsAndKeys.swift */; }; EDC1BC252B0DD88A00211F24 /* IntegrationPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED157C4427CFD99700F22202 /* IntegrationPlugin.swift */; }; - EDC1BC292B0DD88A00211F24 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF592739119600E73142 /* Config.swift */; }; EDC1BC2B2B0DD88A00211F24 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF332739119600E73142 /* API.swift */; }; EDC1BC2D2B0DD88A00211F24 /* RSVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED05631E291BEB0400BAEE65 /* RSVersion.swift */; }; EDC1BC382B0DD88A00211F24 /* TypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3D22BD27A45B6500EC8366 /* TypeAlias.swift */; }; @@ -324,7 +309,6 @@ EDC1BC672B0DD89400211F24 /* ReplayQueuePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3D227A27A3F0C100EC8366 /* ReplayQueuePlugin.swift */; }; EDC1BC692B0DD89400211F24 /* EventsAndKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED277FA027421B06002A8FEA /* EventsAndKeys.swift */; }; EDC1BC6A2B0DD89400211F24 /* IntegrationPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED157C4427CFD99700F22202 /* IntegrationPlugin.swift */; }; - EDC1BC6E2B0DD89400211F24 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF592739119600E73142 /* Config.swift */; }; EDC1BC702B0DD89400211F24 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA7EF332739119600E73142 /* API.swift */; }; EDC1BC722B0DD89400211F24 /* RSVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED05631E291BEB0400BAEE65 /* RSVersion.swift */; }; EDC1BC7D2B0DD89400211F24 /* TypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3D22BD27A45B6500EC8366 /* TypeAlias.swift */; }; @@ -335,17 +319,110 @@ EDC1BC892B0DD89400211F24 /* ContextPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3D227C27A3F0C100EC8366 /* ContextPlugin.swift */; }; EDC1BC8A2B0DD89400211F24 /* Data+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED560388288316C2004B5BEC /* Data+Ext.swift */; }; EDC1BCA52B146A2D00211F24 /* ClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC1327B27D614C400AFD833 /* ClientTests.swift */; }; - EDC1BCAC2B146A2D00211F24 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD0C9662829199200470E88 /* StorageTests.swift */; }; + EDC1BCAC2B146A2D00211F24 /* StorageWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD0C9662829199200470E88 /* StorageWorkerTests.swift */; }; EDC1BCB92B146A3800211F24 /* ClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC1327B27D614C400AFD833 /* ClientTests.swift */; }; - EDC1BCC02B146A3800211F24 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD0C9662829199200470E88 /* StorageTests.swift */; }; + EDC1BCC02B146A3800211F24 /* StorageWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD0C9662829199200470E88 /* StorageWorkerTests.swift */; }; EDC1BCCD2B146A4E00211F24 /* ClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC1327B27D614C400AFD833 /* ClientTests.swift */; }; - EDC1BCD42B146A4E00211F24 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD0C9662829199200470E88 /* StorageTests.swift */; }; + EDC1BCD42B146A4E00211F24 /* StorageWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD0C9662829199200470E88 /* StorageWorkerTests.swift */; }; EDC1BCDE2B14738F00211F24 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC1BCDD2B14738F00211F24 /* Logger.swift */; }; EDC1BCDF2B14738F00211F24 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC1BCDD2B14738F00211F24 /* Logger.swift */; }; EDC1BCE02B14738F00211F24 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC1BCDD2B14738F00211F24 /* Logger.swift */; }; EDC1BCE12B14738F00211F24 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC1BCDD2B14738F00211F24 /* Logger.swift */; }; - EDD0C9672829199200470E88 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD0C9662829199200470E88 /* StorageTests.swift */; }; + EDD0C9672829199200470E88 /* StorageWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD0C9662829199200470E88 /* StorageWorkerTests.swift */; }; EDD3AA3627C659C80048E61E /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD3AA3527C659C80048E61E /* ReadWriteLock.swift */; }; + EDEDC1332B6A02E70082C7E2 /* ClientRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1322B6A02E70082C7E2 /* ClientRegistry.swift */; }; + EDEDC1342B6A02E70082C7E2 /* ClientRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1322B6A02E70082C7E2 /* ClientRegistry.swift */; }; + EDEDC1352B6A02E70082C7E2 /* ClientRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1322B6A02E70082C7E2 /* ClientRegistry.swift */; }; + EDEDC1362B6A02E70082C7E2 /* ClientRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1322B6A02E70082C7E2 /* ClientRegistry.swift */; }; + EDEDC13A2B6B65C10082C7E2 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1392B6B65C10082C7E2 /* Configuration.swift */; }; + EDEDC13B2B6B65C10082C7E2 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1392B6B65C10082C7E2 /* Configuration.swift */; }; + EDEDC13C2B6B65C10082C7E2 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1392B6B65C10082C7E2 /* Configuration.swift */; }; + EDEDC13D2B6B65C10082C7E2 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1392B6B65C10082C7E2 /* Configuration.swift */; }; + EDEDC13F2B6B72AA0082C7E2 /* RSClientCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC13E2B6B72AA0082C7E2 /* RSClientCore.swift */; }; + EDEDC1402B6B72AA0082C7E2 /* RSClientCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC13E2B6B72AA0082C7E2 /* RSClientCore.swift */; }; + EDEDC1412B6B72AA0082C7E2 /* RSClientCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC13E2B6B72AA0082C7E2 /* RSClientCore.swift */; }; + EDEDC1422B6B72AA0082C7E2 /* RSClientCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC13E2B6B72AA0082C7E2 /* RSClientCore.swift */; }; + EDEDC1452B6CC90E0082C7E2 /* StorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1442B6CC90E0082C7E2 /* StorageMigrator.swift */; }; + EDEDC1462B6CC90E0082C7E2 /* StorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1442B6CC90E0082C7E2 /* StorageMigrator.swift */; }; + EDEDC1472B6CC90E0082C7E2 /* StorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1442B6CC90E0082C7E2 /* StorageMigrator.swift */; }; + EDEDC1482B6CC90E0082C7E2 /* StorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1442B6CC90E0082C7E2 /* StorageMigrator.swift */; }; + EDEDC1502B6CFC900082C7E2 /* StorageMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC14F2B6CFC900082C7E2 /* StorageMigration.swift */; }; + EDEDC1512B6CFC900082C7E2 /* StorageMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC14F2B6CFC900082C7E2 /* StorageMigration.swift */; }; + EDEDC1522B6CFC900082C7E2 /* StorageMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC14F2B6CFC900082C7E2 /* StorageMigration.swift */; }; + EDEDC1532B6CFC900082C7E2 /* StorageMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC14F2B6CFC900082C7E2 /* StorageMigration.swift */; }; + EDEDC1552B6D34D10082C7E2 /* StorageMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1542B6D34D10082C7E2 /* StorageMigratorTests.swift */; }; + EDEDC1602B6E5A310082C7E2 /* SQLiteDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC15F2B6E5A310082C7E2 /* SQLiteDatabase.swift */; }; + EDEDC1612B6E5A310082C7E2 /* SQLiteDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC15F2B6E5A310082C7E2 /* SQLiteDatabase.swift */; }; + EDEDC1622B6E5A310082C7E2 /* SQLiteDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC15F2B6E5A310082C7E2 /* SQLiteDatabase.swift */; }; + EDEDC1632B6E5A310082C7E2 /* SQLiteDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC15F2B6E5A310082C7E2 /* SQLiteDatabase.swift */; }; + EDEDC1652B6E5A830082C7E2 /* SQLiteStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1642B6E5A830082C7E2 /* SQLiteStorage.swift */; }; + EDEDC1662B6E5A830082C7E2 /* SQLiteStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1642B6E5A830082C7E2 /* SQLiteStorage.swift */; }; + EDEDC1672B6E5A830082C7E2 /* SQLiteStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1642B6E5A830082C7E2 /* SQLiteStorage.swift */; }; + EDEDC1682B6E5A830082C7E2 /* SQLiteStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1642B6E5A830082C7E2 /* SQLiteStorage.swift */; }; + EDEDC16F2B6F667E0082C7E2 /* SQLiteStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC16E2B6F667E0082C7E2 /* SQLiteStorageTests.swift */; }; + EDEDC1712B6FDFE20082C7E2 /* StorageMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1542B6D34D10082C7E2 /* StorageMigratorTests.swift */; }; + EDEDC1722B6FDFE20082C7E2 /* StorageMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1542B6D34D10082C7E2 /* StorageMigratorTests.swift */; }; + EDEDC1732B6FDFE30082C7E2 /* StorageMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1542B6D34D10082C7E2 /* StorageMigratorTests.swift */; }; + EDEDC1742B6FDFEA0082C7E2 /* SQLiteStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC16E2B6F667E0082C7E2 /* SQLiteStorageTests.swift */; }; + EDEDC1752B6FDFEA0082C7E2 /* SQLiteStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC16E2B6F667E0082C7E2 /* SQLiteStorageTests.swift */; }; + EDEDC1762B6FDFEB0082C7E2 /* SQLiteStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC16E2B6F667E0082C7E2 /* SQLiteStorageTests.swift */; }; + EDEDC1772B6FE0040082C7E2 /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8ED14E2B4C2E5900DAE613 /* URLSessionClientTests.swift */; }; + EDEDC17F2B70B08F0082C7E2 /* MultipleInstanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC17E2B70B08F0082C7E2 /* MultipleInstanceTests.swift */; }; + EDEDC1802B70B08F0082C7E2 /* MultipleInstanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC17E2B70B08F0082C7E2 /* MultipleInstanceTests.swift */; }; + EDEDC1812B70B08F0082C7E2 /* MultipleInstanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC17E2B70B08F0082C7E2 /* MultipleInstanceTests.swift */; }; + EDEDC1822B70B08F0082C7E2 /* MultipleInstanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC17E2B70B08F0082C7E2 /* MultipleInstanceTests.swift */; }; + EDEDC1882B70BCD90082C7E2 /* SQLiteDatabaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1832B70BCCA0082C7E2 /* SQLiteDatabaseMock.swift */; }; + EDEDC1892B70BCDA0082C7E2 /* SQLiteDatabaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1832B70BCCA0082C7E2 /* SQLiteDatabaseMock.swift */; }; + EDEDC18A2B70BCDA0082C7E2 /* SQLiteDatabaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1832B70BCCA0082C7E2 /* SQLiteDatabaseMock.swift */; }; + EDEDC18B2B70BCDB0082C7E2 /* SQLiteDatabaseMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1832B70BCCA0082C7E2 /* SQLiteDatabaseMock.swift */; }; + EDEDC18E2B71139F0082C7E2 /* UserSessionPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC18D2B71139F0082C7E2 /* UserSessionPluginTests.swift */; }; + EDEDC18F2B71139F0082C7E2 /* UserSessionPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC18D2B71139F0082C7E2 /* UserSessionPluginTests.swift */; }; + EDEDC1902B71139F0082C7E2 /* UserSessionPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC18D2B71139F0082C7E2 /* UserSessionPluginTests.swift */; }; + EDEDC1912B71139F0082C7E2 /* UserSessionPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC18D2B71139F0082C7E2 /* UserSessionPluginTests.swift */; }; + EDEDC1942B7212100082C7E2 /* RSClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1932B7212100082C7E2 /* RSClientProtocol.swift */; }; + EDEDC1952B7212100082C7E2 /* RSClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1932B7212100082C7E2 /* RSClientProtocol.swift */; }; + EDEDC1962B7212100082C7E2 /* RSClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1932B7212100082C7E2 /* RSClientProtocol.swift */; }; + EDEDC1972B7212100082C7E2 /* RSClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1932B7212100082C7E2 /* RSClientProtocol.swift */; }; + EDEDC19D2B721AFD0082C7E2 /* RSClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1982B721AF80082C7E2 /* RSClientMock.swift */; }; + EDEDC19E2B721AFD0082C7E2 /* RSClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1982B721AF80082C7E2 /* RSClientMock.swift */; }; + EDEDC19F2B721AFE0082C7E2 /* RSClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1982B721AF80082C7E2 /* RSClientMock.swift */; }; + EDEDC1A02B721AFE0082C7E2 /* RSClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1982B721AF80082C7E2 /* RSClientMock.swift */; }; + EDEDC1A62B722F060082C7E2 /* SessionStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1A12B7220660082C7E2 /* SessionStorageMock.swift */; }; + EDEDC1A72B722F070082C7E2 /* SessionStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1A12B7220660082C7E2 /* SessionStorageMock.swift */; }; + EDEDC1A82B722F070082C7E2 /* SessionStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1A12B7220660082C7E2 /* SessionStorageMock.swift */; }; + EDEDC1A92B722F080082C7E2 /* SessionStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1A12B7220660082C7E2 /* SessionStorageMock.swift */; }; + EDEDC1AC2B7261E50082C7E2 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1AB2B7261E50082C7E2 /* ConsoleLogger.swift */; }; + EDEDC1AD2B7261E50082C7E2 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1AB2B7261E50082C7E2 /* ConsoleLogger.swift */; }; + EDEDC1AE2B7261E50082C7E2 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1AB2B7261E50082C7E2 /* ConsoleLogger.swift */; }; + EDEDC1AF2B7261E50082C7E2 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1AB2B7261E50082C7E2 /* ConsoleLogger.swift */; }; + EDEDC1B12B7262270082C7E2 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1B02B7262270082C7E2 /* LogLevel.swift */; }; + EDEDC1B22B7262270082C7E2 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1B02B7262270082C7E2 /* LogLevel.swift */; }; + EDEDC1B32B7262270082C7E2 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1B02B7262270082C7E2 /* LogLevel.swift */; }; + EDEDC1B42B7262270082C7E2 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1B02B7262270082C7E2 /* LogLevel.swift */; }; + EDEDC1B62B7268060082C7E2 /* EventFilteringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1B52B7268060082C7E2 /* EventFilteringTests.swift */; }; + EDEDC1B72B7268060082C7E2 /* EventFilteringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1B52B7268060082C7E2 /* EventFilteringTests.swift */; }; + EDEDC1B82B7268060082C7E2 /* EventFilteringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1B52B7268060082C7E2 /* EventFilteringTests.swift */; }; + EDEDC1B92B7268060082C7E2 /* EventFilteringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1B52B7268060082C7E2 /* EventFilteringTests.swift */; }; + EDEDC1BB2B728F600082C7E2 /* ReplayQueuePluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1BA2B728F600082C7E2 /* ReplayQueuePluginTests.swift */; }; + EDEDC1BC2B728F600082C7E2 /* ReplayQueuePluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1BA2B728F600082C7E2 /* ReplayQueuePluginTests.swift */; }; + EDEDC1BD2B728F600082C7E2 /* ReplayQueuePluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1BA2B728F600082C7E2 /* ReplayQueuePluginTests.swift */; }; + EDEDC1BE2B728F600082C7E2 /* ReplayQueuePluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1BA2B728F600082C7E2 /* ReplayQueuePluginTests.swift */; }; + EDEDC1C12B7363230082C7E2 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1C02B7363230082C7E2 /* LoggerTests.swift */; }; + EDEDC1C22B7363230082C7E2 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1C02B7363230082C7E2 /* LoggerTests.swift */; }; + EDEDC1C32B7363230082C7E2 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1C02B7363230082C7E2 /* LoggerTests.swift */; }; + EDEDC1C42B7363230082C7E2 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1C02B7363230082C7E2 /* LoggerTests.swift */; }; + EDEDC1C62B7365B90082C7E2 /* ConsoleLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1C52B7365B90082C7E2 /* ConsoleLoggerTests.swift */; }; + EDEDC1C72B7365B90082C7E2 /* ConsoleLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1C52B7365B90082C7E2 /* ConsoleLoggerTests.swift */; }; + EDEDC1C82B7365B90082C7E2 /* ConsoleLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1C52B7365B90082C7E2 /* ConsoleLoggerTests.swift */; }; + EDEDC1C92B7365B90082C7E2 /* ConsoleLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1C52B7365B90082C7E2 /* ConsoleLoggerTests.swift */; }; + EDEDC1CB2B736BEF0082C7E2 /* ConsoleLoggerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1CA2B736BEF0082C7E2 /* ConsoleLoggerMock.swift */; }; + EDEDC1CC2B736BEF0082C7E2 /* ConsoleLoggerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1CA2B736BEF0082C7E2 /* ConsoleLoggerMock.swift */; }; + EDEDC1CD2B736BEF0082C7E2 /* ConsoleLoggerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1CA2B736BEF0082C7E2 /* ConsoleLoggerMock.swift */; }; + EDEDC1CE2B736BEF0082C7E2 /* ConsoleLoggerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1CA2B736BEF0082C7E2 /* ConsoleLoggerMock.swift */; }; + EDEDC1D02B7373BC0082C7E2 /* PrintFunctionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1CF2B7373BC0082C7E2 /* PrintFunctionMock.swift */; }; + EDEDC1D12B7373BC0082C7E2 /* PrintFunctionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1CF2B7373BC0082C7E2 /* PrintFunctionMock.swift */; }; + EDEDC1D22B7373BC0082C7E2 /* PrintFunctionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1CF2B7373BC0082C7E2 /* PrintFunctionMock.swift */; }; + EDEDC1D32B7373BC0082C7E2 /* PrintFunctionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEDC1CF2B7373BC0082C7E2 /* PrintFunctionMock.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -435,10 +512,7 @@ ED8ED1562B4D1D2300DAE613 /* DataResidencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataResidencyTests.swift; sourceTree = ""; }; ED8ED1B42B51483900DAE613 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; ED8ED1B92B51486E00DAE613 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; - ED8ED1BE2B514B4B00DAE613 /* DefaultDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultDatabase.swift; sourceTree = ""; }; - ED8ED1C32B514B6A00DAE613 /* DefaultStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultStorage.swift; sourceTree = ""; }; ED8ED1C82B514B8C00DAE613 /* StorageWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageWorker.swift; sourceTree = ""; }; - ED8ED1D82B53B9F600DAE613 /* Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Controller.swift; sourceTree = ""; }; ED8ED1DD2B55361900DAE613 /* StoragePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoragePlugin.swift; sourceTree = ""; }; ED8ED1EC2B55577F00DAE613 /* DataUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUpload.swift; sourceTree = ""; }; ED8ED1F12B55585F00DAE613 /* DataUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploader.swift; sourceTree = ""; }; @@ -472,7 +546,6 @@ EDA7EF452739119600E73142 /* UserDefaultsWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsWorker.swift; sourceTree = ""; }; EDA7EF482739119600E73142 /* String+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Ext.swift"; sourceTree = ""; }; EDA7EF4F2739119600E73142 /* Rudder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Rudder.h; sourceTree = ""; }; - EDA7EF592739119600E73142 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; EDC1327B27D614C400AFD833 /* ClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientTests.swift; sourceTree = ""; }; EDC132A127D7D61D00AFD833 /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; EDC1BC0D2B0DD87D00211F24 /* Rudder.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Rudder.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -483,8 +556,32 @@ EDC1BCC82B146A3800211F24 /* RudderTests-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "RudderTests-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; EDC1BCDC2B146A4E00211F24 /* RudderTests-macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "RudderTests-macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; EDC1BCDD2B14738F00211F24 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; - EDD0C9662829199200470E88 /* StorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageTests.swift; sourceTree = ""; }; + EDD0C9662829199200470E88 /* StorageWorkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageWorkerTests.swift; sourceTree = ""; }; EDD3AA3527C659C80048E61E /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; + EDEDC1322B6A02E70082C7E2 /* ClientRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientRegistry.swift; sourceTree = ""; }; + EDEDC1392B6B65C10082C7E2 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + EDEDC13E2B6B72AA0082C7E2 /* RSClientCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RSClientCore.swift; sourceTree = ""; }; + EDEDC1442B6CC90E0082C7E2 /* StorageMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMigrator.swift; sourceTree = ""; }; + EDEDC14F2B6CFC900082C7E2 /* StorageMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMigration.swift; sourceTree = ""; }; + EDEDC1542B6D34D10082C7E2 /* StorageMigratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMigratorTests.swift; sourceTree = ""; }; + EDEDC15F2B6E5A310082C7E2 /* SQLiteDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteDatabase.swift; sourceTree = ""; }; + EDEDC1642B6E5A830082C7E2 /* SQLiteStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteStorage.swift; sourceTree = ""; }; + EDEDC16E2B6F667E0082C7E2 /* SQLiteStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLiteStorageTests.swift; sourceTree = ""; }; + EDEDC1792B7095D30082C7E2 /* ObjCRSClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjCRSClient.swift; sourceTree = ""; }; + EDEDC17E2B70B08F0082C7E2 /* MultipleInstanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleInstanceTests.swift; sourceTree = ""; }; + EDEDC1832B70BCCA0082C7E2 /* SQLiteDatabaseMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLiteDatabaseMock.swift; sourceTree = ""; }; + EDEDC18D2B71139F0082C7E2 /* UserSessionPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionPluginTests.swift; sourceTree = ""; }; + EDEDC1932B7212100082C7E2 /* RSClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSClientProtocol.swift; sourceTree = ""; }; + EDEDC1982B721AF80082C7E2 /* RSClientMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSClientMock.swift; sourceTree = ""; }; + EDEDC1A12B7220660082C7E2 /* SessionStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionStorageMock.swift; sourceTree = ""; }; + EDEDC1AB2B7261E50082C7E2 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; + EDEDC1B02B7262270082C7E2 /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; + EDEDC1B52B7268060082C7E2 /* EventFilteringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFilteringTests.swift; sourceTree = ""; }; + EDEDC1BA2B728F600082C7E2 /* ReplayQueuePluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplayQueuePluginTests.swift; sourceTree = ""; }; + EDEDC1C02B7363230082C7E2 /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; + EDEDC1C52B7365B90082C7E2 /* ConsoleLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLoggerTests.swift; sourceTree = ""; }; + EDEDC1CA2B736BEF0082C7E2 /* ConsoleLoggerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLoggerMock.swift; sourceTree = ""; }; + EDEDC1CF2B7373BC0082C7E2 /* PrintFunctionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintFunctionMock.swift; sourceTree = ""; }; EDF7B3F927A80320001C0688 /* SampleBatchPayload.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SampleBatchPayload.json; sourceTree = ""; }; /* End PBXFileReference section */ @@ -640,11 +737,16 @@ ED4082972B610CE1001F371F /* Mocks */ = { isa = PBXGroup; children = ( + ED4082BD2B62404D001F371F /* APIClientMock.swift */, + EDEDC1CA2B736BEF0082C7E2 /* ConsoleLoggerMock.swift */, ED40829E2B610F1F001F371F /* CoreMocks.swift */, + ED4082402B5E79A6001F371F /* NOLogger.swift */, + EDEDC1CF2B7373BC0082C7E2 /* PrintFunctionMock.swift */, + EDEDC1982B721AF80082C7E2 /* RSClientMock.swift */, + EDEDC1A12B7220660082C7E2 /* SessionStorageMock.swift */, ED4082AE2B61FEC2001F371F /* SourceConfigMock.swift */, - ED4082BD2B62404D001F371F /* APIClientMock.swift */, + EDEDC1832B70BCCA0082C7E2 /* SQLiteDatabaseMock.swift */, ED40823B2B5E7615001F371F /* StorageMock.swift */, - ED4082402B5E79A6001F371F /* NOLogger.swift */, ); path = Mocks; sourceTree = ""; @@ -689,8 +791,11 @@ children = ( EDC1327B27D614C400AFD833 /* ClientTests.swift */, ED8ED1562B4D1D2300DAE613 /* DataResidencyTests.swift */, + EDEDC17E2B70B08F0082C7E2 /* MultipleInstanceTests.swift */, ED4082D42B63E1D8001F371F /* DataUpload */, + EDEDC1BF2B7362FC0082C7E2 /* Logger */, ED4082F02B6663D1001F371F /* Networking */, + EDEDC18C2B7113810082C7E2 /* Plugins */, ED4082D12B63E004001F371F /* Policies */, ED4082DA2B656577001F371F /* SourceConfigDownload */, ED4082EF2B6663CA001F371F /* Storage */, @@ -720,7 +825,9 @@ ED4082EF2B6663CA001F371F /* Storage */ = { isa = PBXGroup; children = ( - EDD0C9662829199200470E88 /* StorageTests.swift */, + EDEDC16E2B6F667E0082C7E2 /* SQLiteStorageTests.swift */, + EDEDC1542B6D34D10082C7E2 /* StorageMigratorTests.swift */, + EDD0C9662829199200470E88 /* StorageWorkerTests.swift */, ); path = Storage; sourceTree = ""; @@ -748,9 +855,9 @@ ED8ED19F2B4FCA3C00DAE613 /* Storage */ = { isa = PBXGroup; children = ( - ED8ED1B42B51483900DAE613 /* Database.swift */, - ED8ED1BE2B514B4B00DAE613 /* DefaultDatabase.swift */, - ED8ED1C32B514B6A00DAE613 /* DefaultStorage.swift */, + EDEDC1702B6F8D980082C7E2 /* Database */, + EDEDC14E2B6CFB2B0082C7E2 /* Migration */, + EDEDC1642B6E5A830082C7E2 /* SQLiteStorage.swift */, ED8ED1B92B51486E00DAE613 /* Storage.swift */, ED8ED1C82B514B8C00DAE613 /* StorageWorker.swift */, ); @@ -761,19 +868,23 @@ isa = PBXGroup; children = ( ED3D226227A3F0C100EC8366 /* RSClient.swift */, - ED8ED1D82B53B9F600DAE613 /* Controller.swift */, + EDEDC13E2B6B72AA0082C7E2 /* RSClientCore.swift */, + EDEDC1932B7212100082C7E2 /* RSClientProtocol.swift */, + EDEDC1322B6A02E70082C7E2 /* ClientRegistry.swift */, ED8ED27F2B5A9AF300DAE613 /* Message.swift */, ED8ED2372B590D3D00DAE613 /* PushNotifications.swift */, ED4083142B67CE92001F371F /* ScreenRecording.swift */, + ED4083002B6772BE001F371F /* ApplicationState */, EDD3AA3827C75ECF0048E61E /* Common */, ED8ED1EB2B55573600DAE613 /* DataUpload */, EDD3AA1727C656F90048E61E /* Domain */, - ED8ED1E82B55525600DAE613 /* SourceConfigDownload */, EDD3AA3927C75F250048E61E /* Helpers */, + EDEDC1AA2B7261B80082C7E2 /* Logger */, EDA7EF2E2739119600E73142 /* Networking */, - ED4083002B6772BE001F371F /* ApplicationState */, + EDEDC1782B7095B50082C7E2 /* ObjC */, ED3D226327A3F0C100EC8366 /* Plugins */, ED40822A2B5CEB53001F371F /* Policies */, + ED8ED1E82B55525600DAE613 /* SourceConfigDownload */, ED8ED19F2B4FCA3C00DAE613 /* Storage */, ); path = Core; @@ -885,7 +996,7 @@ EDD3AA3727C659D20048E61E /* Models */ = { isa = PBXGroup; children = ( - EDA7EF592739119600E73142 /* Config.swift */, + EDEDC1392B6B65C10082C7E2 /* Configuration.swift */, ED8ED2702B5A954200DAE613 /* Context.swift */, ED8ED26B2B5A950B00DAE613 /* Option.swift */, ED8ED27A2B5A99A400DAE613 /* StorageMessage.swift */, @@ -910,7 +1021,6 @@ ED8ED23C2B590E8E00DAE613 /* Device.swift */, EDC132A127D7D61D00AFD833 /* JSON.swift */, ED8ED2522B5A348A00DAE613 /* KeyPath.swift */, - EDC1BCDD2B14738F00211F24 /* Logger.swift */, ED8ED2052B5569B800DAE613 /* Reachability.swift */, EDD3AA3527C659C80048E61E /* ReadWriteLock.swift */, ED8ED2572B5A3BC200DAE613 /* SessionStorage.swift */, @@ -920,6 +1030,61 @@ path = Helpers; sourceTree = ""; }; + EDEDC14E2B6CFB2B0082C7E2 /* Migration */ = { + isa = PBXGroup; + children = ( + EDEDC14F2B6CFC900082C7E2 /* StorageMigration.swift */, + EDEDC1442B6CC90E0082C7E2 /* StorageMigrator.swift */, + ); + path = Migration; + sourceTree = ""; + }; + EDEDC1702B6F8D980082C7E2 /* Database */ = { + isa = PBXGroup; + children = ( + ED8ED1B42B51483900DAE613 /* Database.swift */, + EDEDC15F2B6E5A310082C7E2 /* SQLiteDatabase.swift */, + ); + path = Database; + sourceTree = ""; + }; + EDEDC1782B7095B50082C7E2 /* ObjC */ = { + isa = PBXGroup; + children = ( + EDEDC1792B7095D30082C7E2 /* ObjCRSClient.swift */, + ); + path = ObjC; + sourceTree = ""; + }; + EDEDC18C2B7113810082C7E2 /* Plugins */ = { + isa = PBXGroup; + children = ( + EDEDC1B52B7268060082C7E2 /* EventFilteringTests.swift */, + EDEDC18D2B71139F0082C7E2 /* UserSessionPluginTests.swift */, + EDEDC1BA2B728F600082C7E2 /* ReplayQueuePluginTests.swift */, + ); + path = Plugins; + sourceTree = ""; + }; + EDEDC1AA2B7261B80082C7E2 /* Logger */ = { + isa = PBXGroup; + children = ( + EDEDC1AB2B7261E50082C7E2 /* ConsoleLogger.swift */, + EDC1BCDD2B14738F00211F24 /* Logger.swift */, + EDEDC1B02B7262270082C7E2 /* LogLevel.swift */, + ); + path = Logger; + sourceTree = ""; + }; + EDEDC1BF2B7362FC0082C7E2 /* Logger */ = { + isa = PBXGroup; + children = ( + EDEDC1C52B7365B90082C7E2 /* ConsoleLoggerTests.swift */, + EDEDC1C02B7363230082C7E2 /* LoggerTests.swift */, + ); + path = Logger; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1297,6 +1462,7 @@ ED8ED2582B5A3BC200DAE613 /* SessionStorage.swift in Sources */, ED8ED1ED2B55577F00DAE613 /* DataUpload.swift in Sources */, ED5372462B1613CE00D794EB /* LogMessages.swift in Sources */, + EDEDC1332B6A02E70082C7E2 /* ClientRegistry.swift in Sources */, ED8ED1DE2B55361900DAE613 /* StoragePlugin.swift in Sources */, ED8ED2062B5569B800DAE613 /* Reachability.swift in Sources */, ED4083022B6772E6001F371F /* WatchApplicationState.swift in Sources */, @@ -1309,11 +1475,12 @@ ED8ED14A2B456C4F00DAE613 /* Data+Gzip.swift in Sources */, EDD3AA3627C659C80048E61E /* ReadWriteLock.swift in Sources */, ED3D22A427A3F0C100EC8366 /* ReplayQueuePlugin.swift in Sources */, - ED8ED1C42B514B6A00DAE613 /* DefaultStorage.swift in Sources */, + EDEDC1652B6E5A830082C7E2 /* SQLiteStorage.swift in Sources */, ED277FA127421B06002A8FEA /* EventsAndKeys.swift in Sources */, ED8ED2292B590C0900DAE613 /* InternalErrors.swift in Sources */, ED4082312B5E601C001F371F /* EventFiltering.swift in Sources */, ED8ED23D2B590E8E00DAE613 /* Device.swift in Sources */, + EDEDC13F2B6B72AA0082C7E2 /* RSClientCore.swift in Sources */, ED4082A52B6150FE001F371F /* RetryPreset.swift in Sources */, ED8ED26C2B5A950B00DAE613 /* Option.swift in Sources */, ED40825D2B5EE968001F371F /* DestinationDefinition.swift in Sources */, @@ -1321,18 +1488,20 @@ ED8ED2382B590D3D00DAE613 /* PushNotifications.swift in Sources */, ED40825B2B5EE8DB001F371F /* Destination.swift in Sources */, ED4082AA2B615147001F371F /* DownloadUploadRetryStrategy.swift in Sources */, + EDEDC1602B6E5A310082C7E2 /* SQLiteDatabase.swift in Sources */, ED8ED1BA2B51486E00DAE613 /* Storage.swift in Sources */, ED157C4527CFD99700F22202 /* IntegrationPlugin.swift in Sources */, ED8ED2802B5A9AF300DAE613 /* Message.swift in Sources */, ED4082882B60E25C001F371F /* Constants.swift in Sources */, - EDA7EF922739119600E73142 /* Config.swift in Sources */, ED4082FC2B676B43001F371F /* PhoneApplicationState.swift in Sources */, + EDEDC1942B7212100082C7E2 /* RSClientProtocol.swift in Sources */, EDA7EF752739119600E73142 /* API.swift in Sources */, ED4082672B5FB736001F371F /* DataResidency.swift in Sources */, ED8ED2712B5A954200DAE613 /* Context.swift in Sources */, ED4082572B5EE8D3001F371F /* SourceConfig.swift in Sources */, ED4082262B5BBBB4001F371F /* RetryPolicy.swift in Sources */, ED05631F291BEB0400BAEE65 /* RSVersion.swift in Sources */, + EDEDC13A2B6B65C10082C7E2 /* Configuration.swift in Sources */, ED8ED2762B5A987500DAE613 /* UserInfo.swift in Sources */, ED8ED21A2B581E8700DAE613 /* SourceConfigDownloader.swift in Sources */, ED3D22BE27A45B6500EC8366 /* TypeAlias.swift in Sources */, @@ -1340,15 +1509,17 @@ ED8ED2242B58E76200DAE613 /* SourceConfigDownloadWorker.swift in Sources */, EDA7EF842739119600E73142 /* UserDefaultsWorker.swift in Sources */, ED8ED2012B55689300DAE613 /* DownloadUploadBlockers.swift in Sources */, + EDEDC1502B6CFC900082C7E2 /* StorageMigration.swift in Sources */, EDA7EF862739119600E73142 /* String+Ext.swift in Sources */, ED3D22AB27A3F0C100EC8366 /* Plugins.swift in Sources */, - ED8ED1D92B53B9F600DAE613 /* Controller.swift in Sources */, + EDEDC1452B6CC90E0082C7E2 /* StorageMigrator.swift in Sources */, ED4082E12B66058E001F371F /* SourceConfigDownload.swift in Sources */, + EDEDC1B12B7262270082C7E2 /* LogLevel.swift in Sources */, ED3D229427A3F0C100EC8366 /* RSClient.swift in Sources */, - ED8ED1BF2B514B4C00DAE613 /* DefaultDatabase.swift in Sources */, ED4083152B67CE92001F371F /* ScreenRecording.swift in Sources */, EDC1BCDE2B14738F00211F24 /* Logger.swift in Sources */, ED3D22A627A3F0C100EC8366 /* ContextPlugin.swift in Sources */, + EDEDC1AC2B7261E50082C7E2 /* ConsoleLogger.swift in Sources */, ED4082F72B6767FF001F371F /* ApplicationState.swift in Sources */, ED8ED1F22B55585F00DAE613 /* DataUploader.swift in Sources */, ED560389288316C2004B5BEC /* Data+Ext.swift in Sources */, @@ -1363,21 +1534,34 @@ buildActionMask = 2147483647; files = ( ED4082DC2B6565C0001F371F /* SourceConfigDownloadWorkerTests.swift in Sources */, + EDEDC1C62B7365B90082C7E2 /* ConsoleLoggerTests.swift in Sources */, + EDEDC18E2B71139F0082C7E2 /* UserSessionPluginTests.swift in Sources */, ED4082AF2B61FEC2001F371F /* SourceConfigMock.swift in Sources */, EDC1327C27D614C400AFD833 /* ClientTests.swift in Sources */, ED40823C2B5E7615001F371F /* StorageMock.swift in Sources */, ED4082412B5E79A6001F371F /* NOLogger.swift in Sources */, ED40829A2B610E9E001F371F /* Mocks.swift in Sources */, + EDEDC17F2B70B08F0082C7E2 /* MultipleInstanceTests.swift in Sources */, + EDEDC1CB2B736BEF0082C7E2 /* ConsoleLoggerMock.swift in Sources */, ED8ED1882B4D24A100DAE613 /* TestUtils.swift in Sources */, ED4082F22B666936001F371F /* ServiceManagerTests.swift in Sources */, ED4082CD2B63A384001F371F /* RetryPolicyTests.swift in Sources */, + EDEDC1BB2B728F600082C7E2 /* ReplayQueuePluginTests.swift in Sources */, + EDEDC19D2B721AFD0082C7E2 /* RSClientMock.swift in Sources */, ED4082BE2B62404D001F371F /* APIClientMock.swift in Sources */, + EDEDC1B62B7268060082C7E2 /* EventFilteringTests.swift in Sources */, ED4082E62B663015001F371F /* SourceConfigDownloaderTests.swift in Sources */, + EDEDC1A62B722F060082C7E2 /* SessionStorageMock.swift in Sources */, ED4082C32B624E58001F371F /* DataUploadWorkerTests.swift in Sources */, ED8ED14F2B4C2E5900DAE613 /* URLSessionClientTests.swift in Sources */, + EDEDC1C12B7363230082C7E2 /* LoggerTests.swift in Sources */, + EDEDC1882B70BCD90082C7E2 /* SQLiteDatabaseMock.swift in Sources */, + EDEDC16F2B6F667E0082C7E2 /* SQLiteStorageTests.swift in Sources */, + EDEDC1552B6D34D10082C7E2 /* StorageMigratorTests.swift in Sources */, ED40826F2B5FC0C3001F371F /* DataResidencyTests.swift in Sources */, - EDD0C9672829199200470E88 /* StorageTests.swift in Sources */, + EDD0C9672829199200470E88 /* StorageWorkerTests.swift in Sources */, ED4082EB2B663A41001F371F /* SourceConfigDownloadTests.swift in Sources */, + EDEDC1D02B7373BC0082C7E2 /* PrintFunctionMock.swift in Sources */, ED40829F2B610F20001F371F /* CoreMocks.swift in Sources */, ED4082D62B63E253001F371F /* DataUploaderTests.swift in Sources */, ); @@ -1394,6 +1578,7 @@ EDC1BBCE2B0DD87D00211F24 /* JSON.swift in Sources */, ED8ED25B2B5A3BC200DAE613 /* SessionStorage.swift in Sources */, ED8ED1F02B55577F00DAE613 /* DataUpload.swift in Sources */, + EDEDC1362B6A02E70082C7E2 /* ClientRegistry.swift in Sources */, ED5372492B1613CE00D794EB /* LogMessages.swift in Sources */, ED8ED1E12B55361900DAE613 /* StoragePlugin.swift in Sources */, ED8ED2092B5569B800DAE613 /* Reachability.swift in Sources */, @@ -1406,11 +1591,12 @@ ED8ED2182B580B6A00DAE613 /* FlushPolicy.swift in Sources */, ED8ED14D2B456C4F00DAE613 /* Data+Gzip.swift in Sources */, ED40830A2B6772F4001F371F /* MacApplicationState.swift in Sources */, + EDEDC1682B6E5A830082C7E2 /* SQLiteStorage.swift in Sources */, EDC1BBDC2B0DD87D00211F24 /* ReadWriteLock.swift in Sources */, EDC1BBDD2B0DD87D00211F24 /* ReplayQueuePlugin.swift in Sources */, - ED8ED1C72B514B6A00DAE613 /* DefaultStorage.swift in Sources */, EDC1BBDF2B0DD87D00211F24 /* EventsAndKeys.swift in Sources */, ED8ED22C2B590C0900DAE613 /* InternalErrors.swift in Sources */, + EDEDC1422B6B72AA0082C7E2 /* RSClientCore.swift in Sources */, ED4082342B5E601C001F371F /* EventFiltering.swift in Sources */, ED8ED2402B590E8E00DAE613 /* Device.swift in Sources */, ED4082A82B6150FE001F371F /* RetryPreset.swift in Sources */, @@ -1418,18 +1604,20 @@ ED4082602B5EE968001F371F /* DestinationDefinition.swift in Sources */, ED8ED27E2B5A99A400DAE613 /* StorageMessage.swift in Sources */, ED8ED23B2B590D3D00DAE613 /* PushNotifications.swift in Sources */, + EDEDC1632B6E5A310082C7E2 /* SQLiteDatabase.swift in Sources */, ED4082582B5EE8D9001F371F /* Destination.swift in Sources */, ED4082AD2B615147001F371F /* DownloadUploadRetryStrategy.swift in Sources */, ED8ED1BD2B51486E00DAE613 /* Storage.swift in Sources */, EDC1BBE02B0DD87D00211F24 /* IntegrationPlugin.swift in Sources */, ED8ED2832B5A9AF300DAE613 /* Message.swift in Sources */, + EDEDC1972B7212100082C7E2 /* RSClientProtocol.swift in Sources */, ED40828B2B60E25C001F371F /* Constants.swift in Sources */, - EDC1BBE42B0DD87D00211F24 /* Config.swift in Sources */, EDC1BBE62B0DD87D00211F24 /* API.swift in Sources */, ED40826A2B5FB736001F371F /* DataResidency.swift in Sources */, ED8ED2742B5A954200DAE613 /* Context.swift in Sources */, ED4082542B5EE8D1001F371F /* SourceConfig.swift in Sources */, ED4082292B5BBBB4001F371F /* RetryPolicy.swift in Sources */, + EDEDC13D2B6B65C10082C7E2 /* Configuration.swift in Sources */, EDC1BBE82B0DD87D00211F24 /* RSVersion.swift in Sources */, ED8ED2792B5A987500DAE613 /* UserInfo.swift in Sources */, ED8ED21D2B581E8700DAE613 /* SourceConfigDownloader.swift in Sources */, @@ -1437,15 +1625,17 @@ ED8ED2272B58E76200DAE613 /* SourceConfigDownloadWorker.swift in Sources */, EDC1BBF52B0DD87D00211F24 /* UserDefaultsWorker.swift in Sources */, ED8ED2042B55689300DAE613 /* DownloadUploadBlockers.swift in Sources */, + EDEDC1532B6CFC900082C7E2 /* StorageMigration.swift in Sources */, EDC1BBF92B0DD87D00211F24 /* String+Ext.swift in Sources */, EDC1BBFA2B0DD87D00211F24 /* Plugins.swift in Sources */, - ED8ED1DC2B53B9F600DAE613 /* Controller.swift in Sources */, + EDEDC1482B6CC90E0082C7E2 /* StorageMigrator.swift in Sources */, ED4082E42B66058E001F371F /* SourceConfigDownload.swift in Sources */, + EDEDC1B42B7262270082C7E2 /* LogLevel.swift in Sources */, EDC1BBFC2B0DD87D00211F24 /* RSClient.swift in Sources */, - ED8ED1C22B514B4C00DAE613 /* DefaultDatabase.swift in Sources */, ED4083182B67CE92001F371F /* ScreenRecording.swift in Sources */, EDC1BCE12B14738F00211F24 /* Logger.swift in Sources */, EDC1BBFF2B0DD87D00211F24 /* ContextPlugin.swift in Sources */, + EDEDC1AF2B7261E50082C7E2 /* ConsoleLogger.swift in Sources */, ED4082FA2B6767FF001F371F /* ApplicationState.swift in Sources */, ED8ED1F52B55585F00DAE613 /* DataUploader.swift in Sources */, EDC1BC002B0DD87D00211F24 /* Data+Ext.swift in Sources */, @@ -1466,6 +1656,7 @@ EDC1BC132B0DD88A00211F24 /* JSON.swift in Sources */, ED8ED2592B5A3BC200DAE613 /* SessionStorage.swift in Sources */, ED8ED1EE2B55577F00DAE613 /* DataUpload.swift in Sources */, + EDEDC1342B6A02E70082C7E2 /* ClientRegistry.swift in Sources */, ED5372472B1613CE00D794EB /* LogMessages.swift in Sources */, ED8ED1DF2B55361900DAE613 /* StoragePlugin.swift in Sources */, ED8ED2072B5569B800DAE613 /* Reachability.swift in Sources */, @@ -1478,11 +1669,12 @@ ED8ED2162B580B6A00DAE613 /* FlushPolicy.swift in Sources */, ED8ED14B2B456C4F00DAE613 /* Data+Gzip.swift in Sources */, ED4083082B6772F4001F371F /* MacApplicationState.swift in Sources */, + EDEDC1662B6E5A830082C7E2 /* SQLiteStorage.swift in Sources */, EDC1BC212B0DD88A00211F24 /* ReadWriteLock.swift in Sources */, EDC1BC222B0DD88A00211F24 /* ReplayQueuePlugin.swift in Sources */, - ED8ED1C52B514B6A00DAE613 /* DefaultStorage.swift in Sources */, EDC1BC242B0DD88A00211F24 /* EventsAndKeys.swift in Sources */, ED8ED22A2B590C0900DAE613 /* InternalErrors.swift in Sources */, + EDEDC1402B6B72AA0082C7E2 /* RSClientCore.swift in Sources */, ED4082322B5E601C001F371F /* EventFiltering.swift in Sources */, ED8ED23E2B590E8E00DAE613 /* Device.swift in Sources */, ED4082A62B6150FE001F371F /* RetryPreset.swift in Sources */, @@ -1490,18 +1682,20 @@ ED40825E2B5EE968001F371F /* DestinationDefinition.swift in Sources */, ED8ED27C2B5A99A400DAE613 /* StorageMessage.swift in Sources */, ED8ED2392B590D3D00DAE613 /* PushNotifications.swift in Sources */, + EDEDC1612B6E5A310082C7E2 /* SQLiteDatabase.swift in Sources */, ED40825A2B5EE8DA001F371F /* Destination.swift in Sources */, ED4082AB2B615147001F371F /* DownloadUploadRetryStrategy.swift in Sources */, ED8ED1BB2B51486E00DAE613 /* Storage.swift in Sources */, EDC1BC252B0DD88A00211F24 /* IntegrationPlugin.swift in Sources */, ED8ED2812B5A9AF300DAE613 /* Message.swift in Sources */, + EDEDC1952B7212100082C7E2 /* RSClientProtocol.swift in Sources */, ED4082892B60E25C001F371F /* Constants.swift in Sources */, - EDC1BC292B0DD88A00211F24 /* Config.swift in Sources */, EDC1BC2B2B0DD88A00211F24 /* API.swift in Sources */, ED4082682B5FB736001F371F /* DataResidency.swift in Sources */, ED8ED2722B5A954200DAE613 /* Context.swift in Sources */, ED4082562B5EE8D3001F371F /* SourceConfig.swift in Sources */, ED4082272B5BBBB4001F371F /* RetryPolicy.swift in Sources */, + EDEDC13B2B6B65C10082C7E2 /* Configuration.swift in Sources */, EDC1BC2D2B0DD88A00211F24 /* RSVersion.swift in Sources */, ED8ED2772B5A987500DAE613 /* UserInfo.swift in Sources */, ED8ED21B2B581E8700DAE613 /* SourceConfigDownloader.swift in Sources */, @@ -1509,15 +1703,17 @@ ED8ED2252B58E76200DAE613 /* SourceConfigDownloadWorker.swift in Sources */, EDC1BC3A2B0DD88A00211F24 /* UserDefaultsWorker.swift in Sources */, ED8ED2022B55689300DAE613 /* DownloadUploadBlockers.swift in Sources */, + EDEDC1512B6CFC900082C7E2 /* StorageMigration.swift in Sources */, EDC1BC3E2B0DD88A00211F24 /* String+Ext.swift in Sources */, EDC1BC3F2B0DD88A00211F24 /* Plugins.swift in Sources */, - ED8ED1DA2B53B9F600DAE613 /* Controller.swift in Sources */, + EDEDC1462B6CC90E0082C7E2 /* StorageMigrator.swift in Sources */, ED4082E22B66058E001F371F /* SourceConfigDownload.swift in Sources */, + EDEDC1B22B7262270082C7E2 /* LogLevel.swift in Sources */, EDC1BC412B0DD88A00211F24 /* RSClient.swift in Sources */, - ED8ED1C02B514B4C00DAE613 /* DefaultDatabase.swift in Sources */, ED4083162B67CE92001F371F /* ScreenRecording.swift in Sources */, EDC1BCDF2B14738F00211F24 /* Logger.swift in Sources */, EDC1BC442B0DD88A00211F24 /* ContextPlugin.swift in Sources */, + EDEDC1AD2B7261E50082C7E2 /* ConsoleLogger.swift in Sources */, ED4082F82B6767FF001F371F /* ApplicationState.swift in Sources */, ED8ED1F32B55585F00DAE613 /* DataUploader.swift in Sources */, EDC1BC452B0DD88A00211F24 /* Data+Ext.swift in Sources */, @@ -1538,6 +1734,7 @@ EDC1BC582B0DD89400211F24 /* JSON.swift in Sources */, ED8ED25A2B5A3BC200DAE613 /* SessionStorage.swift in Sources */, ED8ED1EF2B55577F00DAE613 /* DataUpload.swift in Sources */, + EDEDC1352B6A02E70082C7E2 /* ClientRegistry.swift in Sources */, ED5372482B1613CE00D794EB /* LogMessages.swift in Sources */, ED8ED1E02B55361900DAE613 /* StoragePlugin.swift in Sources */, ED8ED2082B5569B800DAE613 /* Reachability.swift in Sources */, @@ -1550,11 +1747,12 @@ ED8ED2172B580B6A00DAE613 /* FlushPolicy.swift in Sources */, ED8ED14C2B456C4F00DAE613 /* Data+Gzip.swift in Sources */, ED4083092B6772F4001F371F /* MacApplicationState.swift in Sources */, + EDEDC1672B6E5A830082C7E2 /* SQLiteStorage.swift in Sources */, EDC1BC662B0DD89400211F24 /* ReadWriteLock.swift in Sources */, EDC1BC672B0DD89400211F24 /* ReplayQueuePlugin.swift in Sources */, - ED8ED1C62B514B6A00DAE613 /* DefaultStorage.swift in Sources */, EDC1BC692B0DD89400211F24 /* EventsAndKeys.swift in Sources */, ED8ED22B2B590C0900DAE613 /* InternalErrors.swift in Sources */, + EDEDC1412B6B72AA0082C7E2 /* RSClientCore.swift in Sources */, ED4082332B5E601C001F371F /* EventFiltering.swift in Sources */, ED8ED23F2B590E8E00DAE613 /* Device.swift in Sources */, ED4082A72B6150FE001F371F /* RetryPreset.swift in Sources */, @@ -1562,18 +1760,20 @@ ED40825F2B5EE968001F371F /* DestinationDefinition.swift in Sources */, ED8ED27D2B5A99A400DAE613 /* StorageMessage.swift in Sources */, ED8ED23A2B590D3D00DAE613 /* PushNotifications.swift in Sources */, + EDEDC1622B6E5A310082C7E2 /* SQLiteDatabase.swift in Sources */, ED4082592B5EE8D9001F371F /* Destination.swift in Sources */, ED4082AC2B615147001F371F /* DownloadUploadRetryStrategy.swift in Sources */, ED8ED1BC2B51486E00DAE613 /* Storage.swift in Sources */, EDC1BC6A2B0DD89400211F24 /* IntegrationPlugin.swift in Sources */, ED8ED2822B5A9AF300DAE613 /* Message.swift in Sources */, + EDEDC1962B7212100082C7E2 /* RSClientProtocol.swift in Sources */, ED40828A2B60E25C001F371F /* Constants.swift in Sources */, - EDC1BC6E2B0DD89400211F24 /* Config.swift in Sources */, EDC1BC702B0DD89400211F24 /* API.swift in Sources */, ED4082692B5FB736001F371F /* DataResidency.swift in Sources */, ED8ED2732B5A954200DAE613 /* Context.swift in Sources */, ED4082552B5EE8D2001F371F /* SourceConfig.swift in Sources */, ED4082282B5BBBB4001F371F /* RetryPolicy.swift in Sources */, + EDEDC13C2B6B65C10082C7E2 /* Configuration.swift in Sources */, EDC1BC722B0DD89400211F24 /* RSVersion.swift in Sources */, ED8ED2782B5A987500DAE613 /* UserInfo.swift in Sources */, ED8ED21C2B581E8700DAE613 /* SourceConfigDownloader.swift in Sources */, @@ -1581,15 +1781,17 @@ ED8ED2262B58E76200DAE613 /* SourceConfigDownloadWorker.swift in Sources */, EDC1BC7F2B0DD89400211F24 /* UserDefaultsWorker.swift in Sources */, ED8ED2032B55689300DAE613 /* DownloadUploadBlockers.swift in Sources */, + EDEDC1522B6CFC900082C7E2 /* StorageMigration.swift in Sources */, EDC1BC832B0DD89400211F24 /* String+Ext.swift in Sources */, EDC1BC842B0DD89400211F24 /* Plugins.swift in Sources */, - ED8ED1DB2B53B9F600DAE613 /* Controller.swift in Sources */, + EDEDC1472B6CC90E0082C7E2 /* StorageMigrator.swift in Sources */, ED4082E32B66058E001F371F /* SourceConfigDownload.swift in Sources */, + EDEDC1B32B7262270082C7E2 /* LogLevel.swift in Sources */, EDC1BC862B0DD89400211F24 /* RSClient.swift in Sources */, - ED8ED1C12B514B4C00DAE613 /* DefaultDatabase.swift in Sources */, ED4083172B67CE92001F371F /* ScreenRecording.swift in Sources */, EDC1BCE02B14738F00211F24 /* Logger.swift in Sources */, EDC1BC892B0DD89400211F24 /* ContextPlugin.swift in Sources */, + EDEDC1AE2B7261E50082C7E2 /* ConsoleLogger.swift in Sources */, ED4082F92B6767FF001F371F /* ApplicationState.swift in Sources */, ED8ED1F42B55585F00DAE613 /* DataUploader.swift in Sources */, EDC1BC8A2B0DD89400211F24 /* Data+Ext.swift in Sources */, @@ -1604,20 +1806,34 @@ buildActionMask = 2147483647; files = ( EDC1BCA52B146A2D00211F24 /* ClientTests.swift in Sources */, + EDEDC1C82B7365B90082C7E2 /* ConsoleLoggerTests.swift in Sources */, + EDEDC1902B71139F0082C7E2 /* UserSessionPluginTests.swift in Sources */, ED40823E2B5E7615001F371F /* StorageMock.swift in Sources */, ED4082CF2B63A384001F371F /* RetryPolicyTests.swift in Sources */, ED4082432B5E79A6001F371F /* NOLogger.swift in Sources */, ED4082ED2B663A41001F371F /* SourceConfigDownloadTests.swift in Sources */, ED4082A12B610F20001F371F /* CoreMocks.swift in Sources */, + EDEDC1812B70B08F0082C7E2 /* MultipleInstanceTests.swift in Sources */, + EDEDC1CD2B736BEF0082C7E2 /* ConsoleLoggerMock.swift in Sources */, + EDEDC1752B6FDFEA0082C7E2 /* SQLiteStorageTests.swift in Sources */, ED4082DE2B6565C0001F371F /* SourceConfigDownloadWorkerTests.swift in Sources */, ED4082F42B666936001F371F /* ServiceManagerTests.swift in Sources */, + EDEDC1BD2B728F600082C7E2 /* ReplayQueuePluginTests.swift in Sources */, + EDEDC19F2B721AFE0082C7E2 /* RSClientMock.swift in Sources */, ED4082B12B61FEC2001F371F /* SourceConfigMock.swift in Sources */, + EDEDC1B82B7268060082C7E2 /* EventFilteringTests.swift in Sources */, ED4082E82B663015001F371F /* SourceConfigDownloaderTests.swift in Sources */, + EDEDC1A82B722F070082C7E2 /* SessionStorageMock.swift in Sources */, + EDEDC1722B6FDFE20082C7E2 /* StorageMigratorTests.swift in Sources */, ED8ED18A2B4D24A200DAE613 /* TestUtils.swift in Sources */, + EDEDC1C32B7363230082C7E2 /* LoggerTests.swift in Sources */, + EDEDC18A2B70BCDA0082C7E2 /* SQLiteDatabaseMock.swift in Sources */, ED4082712B5FC0C4001F371F /* DataResidencyTests.swift in Sources */, ED4082C02B62404D001F371F /* APIClientMock.swift in Sources */, + EDEDC1772B6FE0040082C7E2 /* URLSessionClientTests.swift in Sources */, ED4082D82B63E253001F371F /* DataUploaderTests.swift in Sources */, - EDC1BCAC2B146A2D00211F24 /* StorageTests.swift in Sources */, + EDC1BCAC2B146A2D00211F24 /* StorageWorkerTests.swift in Sources */, + EDEDC1D22B7373BC0082C7E2 /* PrintFunctionMock.swift in Sources */, ED40829C2B610E9E001F371F /* Mocks.swift in Sources */, ED4082C52B624E58001F371F /* DataUploadWorkerTests.swift in Sources */, ); @@ -1628,21 +1844,34 @@ buildActionMask = 2147483647; files = ( ED4082DD2B6565C0001F371F /* SourceConfigDownloadWorkerTests.swift in Sources */, + EDEDC1C72B7365B90082C7E2 /* ConsoleLoggerTests.swift in Sources */, + EDEDC18F2B71139F0082C7E2 /* UserSessionPluginTests.swift in Sources */, ED4082B02B61FEC2001F371F /* SourceConfigMock.swift in Sources */, EDC1BCB92B146A3800211F24 /* ClientTests.swift in Sources */, ED40823D2B5E7615001F371F /* StorageMock.swift in Sources */, ED4082422B5E79A6001F371F /* NOLogger.swift in Sources */, ED40829B2B610E9E001F371F /* Mocks.swift in Sources */, + EDEDC1802B70B08F0082C7E2 /* MultipleInstanceTests.swift in Sources */, + EDEDC1CC2B736BEF0082C7E2 /* ConsoleLoggerMock.swift in Sources */, ED8ED1892B4D24A200DAE613 /* TestUtils.swift in Sources */, ED4082F32B666936001F371F /* ServiceManagerTests.swift in Sources */, ED4082CE2B63A384001F371F /* RetryPolicyTests.swift in Sources */, + EDEDC1BC2B728F600082C7E2 /* ReplayQueuePluginTests.swift in Sources */, + EDEDC19E2B721AFD0082C7E2 /* RSClientMock.swift in Sources */, ED4082BF2B62404D001F371F /* APIClientMock.swift in Sources */, + EDEDC1B72B7268060082C7E2 /* EventFilteringTests.swift in Sources */, ED4082E72B663015001F371F /* SourceConfigDownloaderTests.swift in Sources */, + EDEDC1A72B722F070082C7E2 /* SessionStorageMock.swift in Sources */, ED4082C42B624E58001F371F /* DataUploadWorkerTests.swift in Sources */, ED8ED1502B4C2E5900DAE613 /* URLSessionClientTests.swift in Sources */, + EDEDC1C22B7363230082C7E2 /* LoggerTests.swift in Sources */, + EDEDC1892B70BCDA0082C7E2 /* SQLiteDatabaseMock.swift in Sources */, + EDEDC1742B6FDFEA0082C7E2 /* SQLiteStorageTests.swift in Sources */, ED4082702B5FC0C3001F371F /* DataResidencyTests.swift in Sources */, - EDC1BCC02B146A3800211F24 /* StorageTests.swift in Sources */, + EDC1BCC02B146A3800211F24 /* StorageWorkerTests.swift in Sources */, + EDEDC1712B6FDFE20082C7E2 /* StorageMigratorTests.swift in Sources */, ED4082EC2B663A41001F371F /* SourceConfigDownloadTests.swift in Sources */, + EDEDC1D12B7373BC0082C7E2 /* PrintFunctionMock.swift in Sources */, ED4082A02B610F20001F371F /* CoreMocks.swift in Sources */, ED4082D72B63E253001F371F /* DataUploaderTests.swift in Sources */, ); @@ -1653,21 +1882,34 @@ buildActionMask = 2147483647; files = ( EDC1BCCD2B146A4E00211F24 /* ClientTests.swift in Sources */, + EDEDC1C92B7365B90082C7E2 /* ConsoleLoggerTests.swift in Sources */, + EDEDC1912B71139F0082C7E2 /* UserSessionPluginTests.swift in Sources */, ED40823F2B5E7615001F371F /* StorageMock.swift in Sources */, ED4082D02B63A384001F371F /* RetryPolicyTests.swift in Sources */, ED4082442B5E79A6001F371F /* NOLogger.swift in Sources */, ED4082C62B624E58001F371F /* DataUploadWorkerTests.swift in Sources */, ED4082E92B663015001F371F /* SourceConfigDownloaderTests.swift in Sources */, + EDEDC1822B70B08F0082C7E2 /* MultipleInstanceTests.swift in Sources */, + EDEDC1CE2B736BEF0082C7E2 /* ConsoleLoggerMock.swift in Sources */, ED8ED18B2B4D24A300DAE613 /* TestUtils.swift in Sources */, ED4082722B5FC0C4001F371F /* DataResidencyTests.swift in Sources */, ED4082C12B62404D001F371F /* APIClientMock.swift in Sources */, + EDEDC1BE2B728F600082C7E2 /* ReplayQueuePluginTests.swift in Sources */, + EDEDC1A02B721AFE0082C7E2 /* RSClientMock.swift in Sources */, ED4082EE2B663A41001F371F /* SourceConfigDownloadTests.swift in Sources */, + EDEDC1B92B7268060082C7E2 /* EventFilteringTests.swift in Sources */, ED4082B22B61FEC2001F371F /* SourceConfigMock.swift in Sources */, + EDEDC1A92B722F080082C7E2 /* SessionStorageMock.swift in Sources */, ED40829D2B610E9E001F371F /* Mocks.swift in Sources */, ED8ED1512B4C2E5900DAE613 /* URLSessionClientTests.swift in Sources */, - EDC1BCD42B146A4E00211F24 /* StorageTests.swift in Sources */, + EDEDC1C42B7363230082C7E2 /* LoggerTests.swift in Sources */, + EDEDC18B2B70BCDB0082C7E2 /* SQLiteDatabaseMock.swift in Sources */, + EDEDC1762B6FDFEB0082C7E2 /* SQLiteStorageTests.swift in Sources */, + EDC1BCD42B146A4E00211F24 /* StorageWorkerTests.swift in Sources */, ED4082F52B666936001F371F /* ServiceManagerTests.swift in Sources */, + EDEDC1732B6FDFE30082C7E2 /* StorageMigratorTests.swift in Sources */, ED4082A22B610F20001F371F /* CoreMocks.swift in Sources */, + EDEDC1D32B7373BC0082C7E2 /* PrintFunctionMock.swift in Sources */, ED4082DF2B6565C0001F371F /* SourceConfigDownloadWorkerTests.swift in Sources */, ED4082D92B63E253001F371F /* DataUploaderTests.swift in Sources */, ); diff --git a/RudderTests/Core/ClientTests.swift b/RudderTests/Core/ClientTests.swift index bf51d521..5085a7c0 100644 --- a/RudderTests/Core/ClientTests.swift +++ b/RudderTests/Core/ClientTests.swift @@ -12,6 +12,8 @@ import XCTest class ClientTests: XCTestCase { var client: RSClient! + var configuration: Configuration! + var resultPlugin: ResultPlugin! override func setUp() { super.setUp() @@ -19,20 +21,29 @@ class ClientTests: XCTestCase { let userDefaults = UserDefaults(suiteName: #file) userDefaults?.removePersistentDomain(forName: #file) - if let config = Config(writeKey: WRITE_KEY, dataPlaneURL: DATA_PLANE_URL) { - client = RSClient( - config: config, - storage: StorageMock(), - userDefaults: userDefaults, - sourceConfigDownloader: SourceConfigDownloaderMock( - downloadStatus: .mockWith( - needsRetry: false, - responseCode: 200 - ) - ), - logger: NOLogger() - ) - } + configuration = .mockWith( + writeKey: "1234567", + dataPlaneURL: "https://www.rudder.dataplane.com", + flushQueueSize: 12, + dbCountThreshold: 20, + sleepTimeOut: 15, + logLevel: .error, + trackLifecycleEvents: true, + recordScreenViews: false, + controlPlaneURL: "https://www.rudder.controlplane.com", + autoSessionTracking: false, + sessionTimeOut: 5000, + gzipEnabled: false, + dataResidencyServer: .EU + ) + + client = .mockWith( + configuration: configuration, + userDefaults: userDefaults + ) + + resultPlugin = ResultPlugin() + client.addPlugin(resultPlugin) } override func tearDown() { @@ -41,9 +52,6 @@ class ClientTests: XCTestCase { } func testAlias() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.alias("user_id") let aliasMessage1 = resultPlugin.lastMessage as? AliasMessage @@ -64,9 +72,6 @@ class ClientTests: XCTestCase { } func testGroup() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.group("sample_group_id") let groupMessage = resultPlugin.lastMessage as? GroupMessage @@ -78,9 +83,6 @@ class ClientTests: XCTestCase { } func testGroupWithTraits() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.group("sample_group_id", traits: ["key_1": "value_1", "key_2": "value_2"]) let groupMessage = resultPlugin.lastMessage as? GroupMessage @@ -97,9 +99,6 @@ class ClientTests: XCTestCase { } func testIdentify() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.identify("user_id") let identifyMessage = resultPlugin.lastMessage as? IdentifyMessage @@ -109,9 +108,6 @@ class ClientTests: XCTestCase { } func testIdentifyWithTraits() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.identify("user_id", traits: ["email": "abc@def.com"]) let identifyMessage = resultPlugin.lastMessage as? IdentifyMessage @@ -126,9 +122,6 @@ class ClientTests: XCTestCase { } func testUserIdAndTraitsPersistCorrectly() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.identify("user_id", traits: ["email": "abc@def.com"]) let identifyMessage = resultPlugin.lastMessage as? IdentifyMessage @@ -153,9 +146,6 @@ class ClientTests: XCTestCase { } func testScreen() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.screen("ViewController") let screenMessage = resultPlugin.lastMessage as? ScreenMessage @@ -169,9 +159,6 @@ class ClientTests: XCTestCase { } func testScreen_Properties() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.screen("ViewController", properties: ["key_1": "value_1", "key_2": "value_2"]) let screenMessage = resultPlugin.lastMessage as? ScreenMessage @@ -188,9 +175,6 @@ class ClientTests: XCTestCase { } func testScreen_Option() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - let option = MessageOption() .putIntegration("Destination_1", isEnabled: true) .putIntegration("Destination_2", isEnabled: false) @@ -218,9 +202,6 @@ class ClientTests: XCTestCase { } func testScreen_Option_Properties() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - let option = MessageOption() .putIntegration("Destination_1", isEnabled: true) .putIntegration("Destination_2", isEnabled: false) @@ -250,9 +231,6 @@ class ClientTests: XCTestCase { } func testScreen_Category() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.screen("ViewController", category: "category_1") let screenMessage = resultPlugin.lastMessage as? ScreenMessage @@ -266,9 +244,6 @@ class ClientTests: XCTestCase { } func testScreen_Category_Properties() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.screen("ViewController", category: "category_1", properties: ["key_3": "value_3", "key_4": "value_4"]) let screenMessage = resultPlugin.lastMessage as? ScreenMessage @@ -284,9 +259,6 @@ class ClientTests: XCTestCase { } func testScreen_Category_Option() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - let option = MessageOption() .putIntegration("Destination_1", isEnabled: true) .putIntegration("Destination_2", isEnabled: false) @@ -314,9 +286,6 @@ class ClientTests: XCTestCase { } func testScreen_Category_Option_Properties() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - let option = MessageOption() .putIntegration("Destination_1", isEnabled: true) .putIntegration("Destination_2", isEnabled: false) @@ -346,9 +315,6 @@ class ClientTests: XCTestCase { } func testTrack() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - // Track with eventName client.track("simple_track") @@ -362,9 +328,6 @@ class ClientTests: XCTestCase { // Track with eventName and properties func testTrack_Properties() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.track("simple_track_with_props", properties: ["key_1": "value_1", "key_2": "value_2"]) let trackMessage = resultPlugin.lastMessage as? TrackMessage @@ -382,9 +345,6 @@ class ClientTests: XCTestCase { // Track with eventName and option func testTrack_Option() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - let option = MessageOption() .putIntegration("Destination_1", isEnabled: true) .putIntegration("Destination_2", isEnabled: false) @@ -412,9 +372,6 @@ class ClientTests: XCTestCase { // Track with eventName, properties and option func testTrack_Properties_Option() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - let option = MessageOption() .putIntegration("Destination_3", isEnabled: true) .putIntegration("Destination_4", isEnabled: false) @@ -445,44 +402,7 @@ class ClientTests: XCTestCase { XCTAssertTrue(context!["key_2"] as! [String: String] == ["n_key_2": "n_value_2"]) } - // make sure you have Firebase added & enabled to the source in your RudderStack A/C - /*func testDestinationEnabled() { - let expectation = XCTestExpectation(description: "Firebase Expectation") - let myDestination = FirebaseDestination { - expectation.fulfill() - return true - } - - client.addDestination(myDestination) - - - - client.track("testDestinationEnabled") - - wait(for: [expectation], timeout: 2.0) - } - - func testDestinationNotEnabled() { - let expectation = XCTestExpectation(description: "MyDestination Expectation") - let myDestination = MyDestination { - expectation.fulfill() - return true - } - - client.addDestination(myDestination) - - - client.track("testDestinationEnabled") - - XCTExpectFailure { - wait(for: [expectation], timeout: 2.0) - } - }*/ - func testAnonymousId() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.setAnonymousId("anonymous_id") client.track("test_anonymous_id") @@ -499,9 +419,6 @@ class ClientTests: XCTestCase { } func testContext() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.track("context check") let context = resultPlugin.lastMessage?.context @@ -517,9 +434,6 @@ class ClientTests: XCTestCase { } func testDeviceToken() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.setDeviceToken("device_token") client.track("device token check") @@ -531,9 +445,6 @@ class ClientTests: XCTestCase { } func testTraits() { - let resultPlugin = ResultPlugin() - client.addPlugin(resultPlugin) - client.identify("user_id", traits: ["email": "abc@def.com"]) let identifyMessage = resultPlugin.lastMessage as? IdentifyMessage @@ -557,23 +468,67 @@ class ClientTests: XCTestCase { } func testUserId() { + client.identify("user_id", traits: ["email": "abc@def.com"]) + XCTAssertEqual(client.userId, "user_id") } func testConfiguration() { + let config = client.configuration + XCTAssertEqual(config.writeKey, configuration.writeKey) + XCTAssertEqual(config.dataPlaneURL, configuration.dataPlaneURL) + XCTAssertEqual(config.flushQueueSize, configuration.flushQueueSize) + XCTAssertEqual(config.dbCountThreshold, configuration.dbCountThreshold) + XCTAssertEqual(config.sleepTimeOut, configuration.sleepTimeOut) + XCTAssertEqual(config.logLevel, configuration.logLevel) + XCTAssertEqual(config.trackLifecycleEvents, configuration.trackLifecycleEvents) + XCTAssertEqual(config.recordScreenViews, configuration.recordScreenViews) + XCTAssertEqual(config.controlPlaneURL, configuration.controlPlaneURL) + XCTAssertEqual(config.automaticSessionTracking, configuration.automaticSessionTracking) + XCTAssertEqual(config.sessionTimeOut, configuration.sessionTimeOut) + XCTAssertEqual(config.gzipEnabled, configuration.gzipEnabled) + XCTAssertEqual(config.dataResidencyServer, configuration.dataResidencyServer) } - func testOption() { + func testOption() throws { + let globalOption = Option() + .putIntegration("destination_1", isEnabled: true) + .putIntegration("destination_2", isEnabled: true) + .putIntegration("destination_3", isEnabled: false) + client.setOption(globalOption) + + client.track("test_track") + + let trackMessage = resultPlugin.lastMessage as? TrackMessage + + let integrations = try XCTUnwrap(trackMessage?.integrations) + XCTAssertEqual(integrations["destination_1"], true) + XCTAssertEqual(integrations["destination_2"], true) + XCTAssertEqual(integrations["destination_3"], false) } - func testAdvertisingId() { + func testAdvertisingId() throws { + client.setAdvertisingId("advertising_id") + client.track("advertising id check") + let context = resultPlugin.lastMessage?.context + let advertisingId = context?[keyPath: "device.advertisingId"] as? String + let adTrackingEnabled = try XCTUnwrap(context?[keyPath: "device.adTrackingEnabled"] as? Bool) + + XCTAssertTrue(advertisingId == "advertising_id") + XCTAssertTrue(adTrackingEnabled) } - func testAppTrackingConsent() { + func testAppTrackingConsent() throws { + client.setAppTrackingConsent(.authorize) + client.track("advertising id check") + + let context = resultPlugin.lastMessage?.context + let attTrackingStatus = try XCTUnwrap(context?[keyPath: "device.attTrackingStatus"] as? Int) + XCTAssertEqual(attTrackingStatus, 3) } func testOptOut() { @@ -584,7 +539,7 @@ class ClientTests: XCTestCase { class ResultPlugin: Plugin { var sourceConfig: SourceConfig? var type: PluginType = .default - var client: RSClient? + var client: RSClientProtocol? var lastMessage: Message? var trackList = [TrackMessage]() diff --git a/RudderTests/Core/DataResidencyTests.swift b/RudderTests/Core/DataResidencyTests.swift index 8297e464..12ac7dd0 100644 --- a/RudderTests/Core/DataResidencyTests.swift +++ b/RudderTests/Core/DataResidencyTests.swift @@ -16,7 +16,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_1() { let sourceConfig: SourceConfig = MultiDataResidency.defaultTrue - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -25,7 +25,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_2() { let sourceConfig: SourceConfig = MultiDataResidency.defaultTrue - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -34,7 +34,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_3() { let sourceConfig: SourceConfig = MultiDataResidency.defaultTrue - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -43,7 +43,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_DefaultFalse_1() { let sourceConfig: SourceConfig = MultiDataResidency.defaultFalse - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -51,7 +51,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_DefaultFalse_2() { let sourceConfig: SourceConfig = MultiDataResidency.defaultFalse - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -59,7 +59,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_DefaultFalse_3() { let sourceConfig: SourceConfig = MultiDataResidency.defaultFalse - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -67,7 +67,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_USTrue_1() { let sourceConfig: SourceConfig = MultiDataResidency.USTrue - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -76,7 +76,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_USTrue_2() { let sourceConfig: SourceConfig = MultiDataResidency.USTrue - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -84,7 +84,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_USTrue_3() { let sourceConfig: SourceConfig = MultiDataResidency.USTrue - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -93,7 +93,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_EUTrue_1() { let sourceConfig: SourceConfig = MultiDataResidency.EUTrue - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -101,7 +101,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_EUTrue_2() { let sourceConfig: SourceConfig = MultiDataResidency.EUTrue - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -110,7 +110,7 @@ final class DataResidencyTests: XCTestCase { func testWithBothResidenciesInSourceConfig_EUTrue_3() { let sourceConfig: SourceConfig = MultiDataResidency.EUTrue - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -118,7 +118,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyUSInSourceConfig_1() { let sourceConfig: SourceConfig = USDataResidency.defaultTrue - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -127,7 +127,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyUSInSourceConfig_2() { let sourceConfig: SourceConfig = USDataResidency.defaultTrue - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -136,7 +136,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyUSInSourceConfig_3() { let sourceConfig: SourceConfig = USDataResidency.defaultTrue - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -145,7 +145,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyUSInSourceConfig_DefaultFalse_1() { let sourceConfig: SourceConfig = USDataResidency.defaultFalse - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -153,7 +153,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyUSInSourceConfig_DefaultFalse_2() { let sourceConfig: SourceConfig = USDataResidency.defaultFalse - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -161,7 +161,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyUSInSourceConfig_DefaultFalse_3() { let sourceConfig: SourceConfig = USDataResidency.defaultFalse - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -169,7 +169,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyEUInSourceConfig_1() { let sourceConfig: SourceConfig = EUDataResidency.defaultTrue - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -177,7 +177,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyEUInSourceConfig_2() { let sourceConfig: SourceConfig = EUDataResidency.defaultTrue - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNotNil(dataResidency.dataPlaneUrl) @@ -186,7 +186,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyEUInSourceConfig_3() { let sourceConfig: SourceConfig = EUDataResidency.defaultTrue - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -194,7 +194,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyEUInSourceConfig_DefaultFalse_1() { let sourceConfig: SourceConfig = EUDataResidency.defaultFalse - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -202,7 +202,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyEUInSourceConfig_DefaultFalse_2() { let sourceConfig: SourceConfig = EUDataResidency.defaultFalse - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -210,7 +210,7 @@ final class DataResidencyTests: XCTestCase { func testWithOnlyEUInSourceConfig_DefaultFalse_3() { let sourceConfig: SourceConfig = EUDataResidency.defaultFalse - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -222,7 +222,7 @@ final class DataResidencyTests: XCTestCase { dataPlanes: nil ) ) - let config: Config = .mockWith() + let config: Configuration = .mockWith() let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -234,7 +234,7 @@ final class DataResidencyTests: XCTestCase { dataPlanes: nil ) ) - let config: Config = .mockWith(dataResidencyServer: .EU) + let config: Configuration = .mockWith(dataResidencyServer: .EU) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) @@ -246,14 +246,14 @@ final class DataResidencyTests: XCTestCase { dataPlanes: nil ) ) - let config: Config = .mockWith(dataResidencyServer: .US) + let config: Configuration = .mockWith(dataResidencyServer: .US) let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) XCTAssertNil(dataResidency.dataPlaneUrl) } func testWhenNoUrlInSourceConfig_4() { - let config: Config? = Config(writeKey: WRITE_KEY, dataPlaneURL: "https::/dataplanerudderstackcom") + let config: Configuration? = Configuration(writeKey: WRITE_KEY, dataPlaneURL: "https::/dataplanerudderstackcom") XCTAssertNil(config) } } diff --git a/RudderTests/Core/DataUpload/DataUploadWorkerTests.swift b/RudderTests/Core/DataUpload/DataUploadWorkerTests.swift index 660b1909..d8c83f7a 100644 --- a/RudderTests/Core/DataUpload/DataUploadWorkerTests.swift +++ b/RudderTests/Core/DataUpload/DataUploadWorkerTests.swift @@ -11,7 +11,7 @@ import XCTest final class DataUploadWorkerTests: XCTestCase { - let storageWorker = DefaultStorageWorker(storage: StorageMock(), queue: DispatchQueue(label: "dataUploadWorkerTests.storageWorker".queueLabel())) + let storageWorker = StorageWorker(storage: StorageMock(), queue: DispatchQueue(label: "dataUploadWorkerTests.storageWorker".queueLabel())) let queue = DispatchQueue( label: "dataUploadWorkerTests".queueLabel(), target: .global(qos: .utility) @@ -42,11 +42,11 @@ final class DataUploadWorkerTests: XCTestCase { ) // Given - storageWorker.saveMessage(StorageMessage(id: "1", message: "message_1")) - storageWorker.saveMessage(StorageMessage(id: "2", message: "message_2")) - storageWorker.saveMessage(StorageMessage(id: "3", message: "message_3")) - storageWorker.saveMessage(StorageMessage(id: "4", message: "message_4")) - storageWorker.saveMessage(StorageMessage(id: "5", message: "message_5")) + storageWorker.saveMessage(StorageMessage(id: "1", message: "message_1", updated: 1234567890)) + storageWorker.saveMessage(StorageMessage(id: "2", message: "message_2", updated: 1234567890)) + storageWorker.saveMessage(StorageMessage(id: "3", message: "message_3", updated: 1234567890)) + storageWorker.saveMessage(StorageMessage(id: "4", message: "message_4", updated: 1234567890)) + storageWorker.saveMessage(StorageMessage(id: "5", message: "message_5", updated: 1234567890)) // When let worker = DataUploadWorker( @@ -101,7 +101,7 @@ final class DataUploadWorkerTests: XCTestCase { ) ) - storageWorker.saveMessage(StorageMessage(id: "1", message: "message_1")) + storageWorker.saveMessage(StorageMessage(id: "1", message: "message_1", updated: 1234567890)) XCTAssertEqual(storageWorker.getMessageCount(), 1) // When diff --git a/RudderTests/Core/Logger/ConsoleLoggerTests.swift b/RudderTests/Core/Logger/ConsoleLoggerTests.swift new file mode 100644 index 00000000..8b548f03 --- /dev/null +++ b/RudderTests/Core/Logger/ConsoleLoggerTests.swift @@ -0,0 +1,106 @@ +// +// ConsoleLoggerTests.swift +// Rudder +// +// Created by Pallab Maiti on 07/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + +final class ConsoleLoggerTests: XCTestCase { + let printFunctionMock = PrintFunctionMock() + + func test_debug() { + // Given + let consoleLogger = ConsoleLogger(logLevel: .debug, instanceName: "test", printFunction: printFunctionMock.print) + + // When + consoleLogger.log("debug_message", logLevel: .debug) + consoleLogger.log("error_message", logLevel: .error) + consoleLogger.log("warning_message", logLevel: .warning) + consoleLogger.log("info_message", logLevel: .info) + + // Then + XCTAssertEqual(printFunctionMock.printedMessages.count, 1) + XCTAssertEqual(printFunctionMock.printedMessage, "[RUDDERSTACK SDK] - DEBUG | [test] - ConsoleLoggerTests.swift:\(#function):20 | debug_message") + } + + func test_error() { + // Given + let consoleLogger = ConsoleLogger(logLevel: .error, instanceName: "test", printFunction: printFunctionMock.print) + + // When + consoleLogger.log("debug_message", logLevel: .debug) + consoleLogger.log("error_message", logLevel: .error) + consoleLogger.log("warning_message", logLevel: .warning) + consoleLogger.log("info_message", logLevel: .info) + + // Then + XCTAssertEqual(printFunctionMock.printedMessages.count, 1) + XCTAssertEqual(printFunctionMock.printedMessages[0], "[RUDDERSTACK SDK] - ERROR | [test] - ConsoleLoggerTests.swift:\(#function):36 | error_message") + } + + func test_warning() { + // Given + let consoleLogger = ConsoleLogger(logLevel: .warning, instanceName: "test", printFunction: printFunctionMock.print) + + // When + consoleLogger.log("debug_message", logLevel: .debug) + consoleLogger.log("error_message", logLevel: .error) + consoleLogger.log("warning_message", logLevel: .warning) + consoleLogger.log("info_message", logLevel: .info) + + // Then + XCTAssertEqual(printFunctionMock.printedMessages.count, 1) + XCTAssertEqual(printFunctionMock.printedMessages[0], "[RUDDERSTACK SDK] - WARN | [test] - ConsoleLoggerTests.swift:\(#function):52 | warning_message") + } + + func test_info() { + // Given + let consoleLogger = ConsoleLogger(logLevel: .info, instanceName: "test", printFunction: printFunctionMock.print) + + // When + consoleLogger.log("debug_message", logLevel: .debug) + consoleLogger.log("error_message", logLevel: .error) + consoleLogger.log("warning_message", logLevel: .warning) + consoleLogger.log("info_message", logLevel: .info) + + // Then + XCTAssertEqual(printFunctionMock.printedMessages.count, 1) + XCTAssertEqual(printFunctionMock.printedMessages[0], "[RUDDERSTACK SDK] - INFO | [test] - ConsoleLoggerTests.swift:\(#function):68 | info_message") + } + + func test_verbose() { + // Given + let consoleLogger = ConsoleLogger(logLevel: .verbose, instanceName: "test", printFunction: printFunctionMock.print) + + // When + consoleLogger.log("debug_message", logLevel: .debug) + consoleLogger.log("error_message", logLevel: .error) + consoleLogger.log("warning_message", logLevel: .warning) + consoleLogger.log("info_message", logLevel: .info) + + // Then + XCTAssertEqual(printFunctionMock.printedMessages.count, 4) + XCTAssertEqual(printFunctionMock.printedMessages[0], "[RUDDERSTACK SDK] - DEBUG | [test] - ConsoleLoggerTests.swift:\(#function):80 | debug_message") + XCTAssertEqual(printFunctionMock.printedMessages[1], "[RUDDERSTACK SDK] - ERROR | [test] - ConsoleLoggerTests.swift:\(#function):81 | error_message") + XCTAssertEqual(printFunctionMock.printedMessages[2], "[RUDDERSTACK SDK] - WARN | [test] - ConsoleLoggerTests.swift:\(#function):82 | warning_message") + XCTAssertEqual(printFunctionMock.printedMessages[3], "[RUDDERSTACK SDK] - INFO | [test] - ConsoleLoggerTests.swift:\(#function):83 | info_message") + } + + func test_none() { + // Given + let consoleLogger = ConsoleLogger(logLevel: .none, instanceName: "test", printFunction: printFunctionMock.print) + + // When + consoleLogger.log("debug_message", logLevel: .debug) + consoleLogger.log("error_message", logLevel: .error) + consoleLogger.log("warning_message", logLevel: .warning) + consoleLogger.log("info_message", logLevel: .info) + + // Then + XCTAssertEqual(printFunctionMock.printedMessages.count, 0) + } +} diff --git a/RudderTests/Core/Logger/LoggerTests.swift b/RudderTests/Core/Logger/LoggerTests.swift new file mode 100644 index 00000000..aa042af2 --- /dev/null +++ b/RudderTests/Core/Logger/LoggerTests.swift @@ -0,0 +1,137 @@ +// +// LoggerTests.swift +// Rudder +// +// Created by Pallab Maiti on 07/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + +final class LoggerTests: XCTestCase { + func test_debug() { + // Given + let consoleLogger = ConsoleLoggerMock(logLevel: .debug) + let logger = Logger(logger: consoleLogger) + + // When & Then + logger.logDebug(LogMessages.customMessage("test_message")) + XCTAssertEqual(consoleLogger.logMessage, "test_message") + + logger.logDebug("test_message_2") + XCTAssertEqual(consoleLogger.logMessage, "test_message_2") + + logger.logError("test_message_3") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logWarning("test_message_4") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logInfo("test_message_5") + XCTAssertEqual(consoleLogger.logMessage, "") + } + + func test_error() { + // Given + let consoleLogger = ConsoleLoggerMock(logLevel: .error) + let logger = Logger(logger: consoleLogger) + + // When & Then + logger.logDebug("test_message") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logError(LogMessages.customMessage("test_message_2")) + XCTAssertEqual(consoleLogger.logMessage, "test_message_2") + + logger.logError("test_message_3") + XCTAssertEqual(consoleLogger.logMessage, "test_message_3") + + logger.logWarning("test_message_4") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logInfo("test_message_5") + XCTAssertEqual(consoleLogger.logMessage, "") + } + + func test_warning() { + // Given + let consoleLogger = ConsoleLoggerMock(logLevel: .warning) + let logger = Logger(logger: consoleLogger) + + logger.logDebug("test_message") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logError("test_message_2") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logWarning(.customMessage("test_message_3")) + XCTAssertEqual(consoleLogger.logMessage, "test_message_3") + + logger.logWarning("test_message_4") + XCTAssertEqual(consoleLogger.logMessage, "test_message_4") + + logger.logInfo("test_message_5") + XCTAssertEqual(consoleLogger.logMessage, "") + } + + func test_info() { + // Given + let consoleLogger = ConsoleLoggerMock(logLevel: .info) + let logger = Logger(logger: consoleLogger) + + // When & Then + logger.logDebug("test_message") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logError("test_message_2") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logWarning("test_message_3") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logInfo(LogMessages.customMessage("test_message_4")) + XCTAssertEqual(consoleLogger.logMessage, "test_message_4") + + logger.logInfo("test_message_5") + XCTAssertEqual(consoleLogger.logMessage, "test_message_5") + } + + func test_verbose() { + // Given + let consoleLogger = ConsoleLoggerMock(logLevel: .verbose) + let logger = Logger(logger: consoleLogger) + + // When & Then + logger.logDebug("test_message_2") + XCTAssertEqual(consoleLogger.logMessage, "test_message_2") + + logger.logError("test_message_3") + XCTAssertEqual(consoleLogger.logMessage, "test_message_3") + + logger.logWarning("test_message_4") + XCTAssertEqual(consoleLogger.logMessage, "test_message_4") + + logger.logInfo("test_message_5") + XCTAssertEqual(consoleLogger.logMessage, "test_message_5") + } + + func test_none() { + // Given + let consoleLogger = ConsoleLoggerMock(logLevel: .none) + let logger = Logger(logger: consoleLogger) + + // When & Then + logger.logDebug("test_message_2") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logError("test_message_3") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logWarning("test_message_4") + XCTAssertEqual(consoleLogger.logMessage, "") + + logger.logInfo("test_message_5") + XCTAssertEqual(consoleLogger.logMessage, "") + } +} diff --git a/RudderTests/Core/MultipleInstanceTests.swift b/RudderTests/Core/MultipleInstanceTests.swift new file mode 100644 index 00000000..d36bfb68 --- /dev/null +++ b/RudderTests/Core/MultipleInstanceTests.swift @@ -0,0 +1,138 @@ +// +// MultipleInstanceTests.swift +// Rudder +// +// Created by Pallab Maiti on 05/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + +final class MultipleInstanceTests: XCTestCase { + func test_default() { + let instance: RSClient = .mockAny() + + defer { + instance.flush() + } + + XCTAssertNotNil(ClientRegistry.default) + XCTAssertNotNil(RSClient.default) + ClientRegistry.unregisterDefault() + } + + func test_returnSameDefaultInstance() { + let instance_1: RSClient = .mockAny() + _ = RSClient.mockAny() + _ = RSClient.mockAny() + + defer { + instance_1.flush() + } + + XCTAssertEqual(ClientRegistry.instances.count, 1) + ClientRegistry.unregisterDefault() + } + + func test_returnSameCustomInstance() { + let instance_1: RSClient = .mockWith(instanceName: "test_1") + _ = RSClient.mockWith(instanceName: "test_1") + + defer { + instance_1.flush() + } + + XCTAssertNil(RSClient.default) + XCTAssertEqual(ClientRegistry.instances.count, 1) + RSClient.unregisterInstance(named: "test_1") + } + + func test_multiple_withDefault() { + let instance_1: RSClient = .mockAny() + let instance_2: RSClient = .mockWith(instanceName: "test_1") + let instance_3: RSClient = .mockWith(instanceName: "test_2") + + defer { + instance_1.flush() + instance_2.flush() + instance_3.flush() + } + + XCTAssertNotNil(ClientRegistry.default) + XCTAssertEqual(ClientRegistry.instances.count, 3) + + ClientRegistry.unregisterDefault() + RSClient.unregisterInstance(named: "test_1") + RSClient.unregisterInstance(named: "test_2") + } + + func test_multiple_withoutDefault() { + let instance_1: RSClient = .mockWith(instanceName: "test_1") + let instance_2: RSClient = .mockWith(instanceName: "test_2") + + defer { + instance_1.flush() + instance_2.flush() + } + + XCTAssertNil(RSClient.default) + XCTAssertEqual(ClientRegistry.instances.count, 2) + + RSClient.unregisterInstance(named: "test_1") + RSClient.unregisterInstance(named: "test_2") + } + + func test_emptyInstanceName() { + let instance: RSClient = .mockWith(instanceName: "") + + defer { + instance.flush() + } + + XCTAssertNotNil(RSClient.default) + XCTAssertEqual(ClientRegistry.instances.count, 1) + + ClientRegistry.unregisterDefault() + } + + func test_instance() { + let instance: RSClient = .mockWith(instanceName: "test_1") + + defer { + instance.flush() + } + + let expectedInstance = RSClient.instance(named: "test_1") + + XCTAssertNotNil(expectedInstance) + + RSClient.unregisterInstance(named: "test_1") + } + + func test_isRegistered() { + let instance: RSClient = .mockWith(instanceName: "test_1") + + defer { + instance.flush() + } + + XCTAssertTrue(RSClient.isRegistered(instanceName: "test_1")) + + RSClient.unregisterInstance(named: "test_1") + } + + func test_unregisterInstance() { + let instance: RSClient = .mockWith(instanceName: "test_1") + + defer { + instance.flush() + } + + XCTAssertEqual(ClientRegistry.instances.count, 1) + + RSClient.unregisterInstance(named: "test_1") + + XCTAssertEqual(ClientRegistry.instances.count, 0) + } +} diff --git a/RudderTests/Core/Networking/URLSessionClientTests.swift b/RudderTests/Core/Networking/URLSessionClientTests.swift index 5ed9c08c..badba5eb 100644 --- a/RudderTests/Core/Networking/URLSessionClientTests.swift +++ b/RudderTests/Core/Networking/URLSessionClientTests.swift @@ -30,6 +30,7 @@ final class URLSessionClientTests: XCTestCase { waitForExpectations(timeout: 1.0) } + #if !os(watchOS) func test_ReturnsError() { let server = ServerMock(serverResult: .failure(error: NSError(domain: "Mock Error", code: 500))) let client = URLSessionClient(session: server.getInterceptedURLSession()) @@ -39,7 +40,7 @@ final class URLSessionClientTests: XCTestCase { client.send(request: .mockAny()) { result in switch result { case .success: - XCTFail("Server shouldn't return response") + XCTFail("Server shouldn't return response") case .failure(let error): XCTAssertEqual((error as NSError).domain, "Mock Error") XCTAssertEqual((error as NSError).code, 500) @@ -49,5 +50,6 @@ final class URLSessionClientTests: XCTestCase { waitForExpectations(timeout: 1.0) } + #endif } diff --git a/RudderTests/Core/Plugins/EventFilteringTests.swift b/RudderTests/Core/Plugins/EventFilteringTests.swift new file mode 100644 index 00000000..b4f3033b --- /dev/null +++ b/RudderTests/Core/Plugins/EventFilteringTests.swift @@ -0,0 +1,139 @@ +// +// EventFilteringTests.swift +// Rudder +// +// Created by Pallab Maiti on 06/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + +final class EventFilteringTests: XCTestCase { + func test_whiteListEvent() throws { + let destination = EventFilteringTestDestination() + // Given + let config = try JSON([ + "whitelistedEvents": [ + [ + "eventName": "whitelist_1" + ] + ], + "eventFilteringOption": "whitelistedEvents" + + ]) + destination.sourceConfig = .mockWith( + source: .mockWith( + destinations: [.mockWith( + config: config, + enabled: true, + destinationDefinition: .mockWith( + displayName: "test_destination" + ) + )] + ) + ) + + // When & Then + _ = destination.process(message: TrackMessage(event: "whitelist_1")) + XCTAssertNotNil(destination.lastMessage) + XCTAssertEqual(destination.lastMessage?.event, "whitelist_1") + + _ = destination.process(message: TrackMessage(event: "whitelist_2")) + XCTAssertEqual(destination.lastMessage?.event, "whitelist_1") + + _ = destination.process(message: TrackMessage(event: "whitelist_3")) + XCTAssertEqual(destination.lastMessage?.event, "whitelist_1") + } + + func test_blackListEvent() throws { + let destination = EventFilteringTestDestination() + // Given + let config = try JSON([ + "blacklistedEvents": [ + [ + "eventName": "blacklist_1" + ] + ], + "eventFilteringOption": "blacklistedEvents" + + ]) + destination.sourceConfig = .mockWith( + source: .mockWith( + destinations: [.mockWith( + config: config, + enabled: true, + destinationDefinition: .mockWith( + displayName: "test_destination" + ) + )] + ) + ) + + // When & Then + _ = destination.process(message: TrackMessage(event: "whitelist_1")) + XCTAssertNotNil(destination.lastMessage) + XCTAssertEqual(destination.lastMessage?.event, "whitelist_1") + + _ = destination.process(message: TrackMessage(event: "blacklist_1")) + XCTAssertEqual(destination.lastMessage?.event, "whitelist_1") + + _ = destination.process(message: TrackMessage(event: "whitelist_3")) + XCTAssertEqual(destination.lastMessage?.event, "whitelist_3") + } + + func test_disabled() throws { + let destination = EventFilteringTestDestination() + // Given + let config = try JSON([ + "blacklistedEvents": [ + [ + "eventName": "blacklist_1" + ] + ], + "whitelistedEvents": [ + [ + "eventName": "whitelist_1" + ] + ], + "eventFilteringOption": "disabled" + ]) + destination.sourceConfig = .mockWith( + source: .mockWith( + destinations: [.mockWith( + config: config, + enabled: true, + destinationDefinition: .mockWith( + displayName: "test_destination" + ) + )] + ) + ) + + // When & Then + _ = destination.process(message: TrackMessage(event: "whitelist_1")) + XCTAssertNotNil(destination.lastMessage) + XCTAssertEqual(destination.lastMessage?.event, "whitelist_1") + + _ = destination.process(message: TrackMessage(event: "blacklist_1")) + XCTAssertEqual(destination.lastMessage?.event, "blacklist_1") + + _ = destination.process(message: TrackMessage(event: "whitelist_3")) + XCTAssertEqual(destination.lastMessage?.event, "whitelist_3") + } +} + +class EventFilteringTestDestination: DestinationPlugin { + var name: String = "test_destination" + var plugins: [Rudder.Plugin] = [] + var type: Rudder.PluginType = .destination + var client: Rudder.RSClientProtocol? + var sourceConfig: Rudder.SourceConfig? + + var lastMessage: TrackMessage? + + func track(message: TrackMessage) -> TrackMessage? { + lastMessage = message + return message + } +} diff --git a/RudderTests/Core/Plugins/ReplayQueuePluginTests.swift b/RudderTests/Core/Plugins/ReplayQueuePluginTests.swift new file mode 100644 index 00000000..a29dc5dd --- /dev/null +++ b/RudderTests/Core/Plugins/ReplayQueuePluginTests.swift @@ -0,0 +1,305 @@ +// +// ReplayQueuePluginTests.swift +// Rudder +// +// Created by Pallab Maiti on 06/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + +final class ReplayQueueTests: XCTestCase { + func test_replayQueue_withNoCachedSourceConfig_andNewSourceConfigEnabledTrue() { + let processMessageExpectation = expectation(description: "Process 5 messages") + processMessageExpectation.expectedFulfillmentCount = 5 + + let updateSourceConfigExpectation = expectation(description: "Update SourceConfig") + updateSourceConfigExpectation.expectedFulfillmentCount = 1 + + let client = RSClientMock() + + let replayQueuePlugin = ReplayQueuePlugin(queue: DispatchQueue(label: "replayQueueTests_1".queueLabel())) + replayQueuePlugin.client = client + client.addPlugin(replayQueuePlugin) + + let destinationPlugin = ReplayQueueTestDestination( + onProcessMessage: processMessageExpectation.fulfill, + onUpdateSourceConfig: updateSourceConfigExpectation.fulfill + ) + destinationPlugin.client = client + client.addPlugin(destinationPlugin) + + client.track("test_track", properties: nil, option: nil) + client.screen("test_screen", category: nil, properties: nil, option: nil) + client.identify("test_user_id", traits: nil, option: nil) + client.alias("test_new_id", option: nil) + client.group("test_group_id", traits: nil, option: nil) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: true + ) + )) + + wait(for: [processMessageExpectation, updateSourceConfigExpectation], timeout: 1.0) + } + + func test_replayQueue_withNoCachedSourceConfig_andNewSourceConfigEnabledFalse() { + let processMessageExpectation = expectation(description: "Process 5 messages") + processMessageExpectation.expectedFulfillmentCount = 5 + processMessageExpectation.isInverted = true + + let updateSourceConfigExpectation = expectation(description: "Update SourceConfig") + updateSourceConfigExpectation.expectedFulfillmentCount = 1 + + let client = RSClientMock() + + let replayQueuePlugin = ReplayQueuePlugin(queue: DispatchQueue(label: "replayQueueTests_1".queueLabel())) + replayQueuePlugin.client = client + client.addPlugin(replayQueuePlugin) + + let destinationPlugin = ReplayQueueTestDestination( + onProcessMessage: processMessageExpectation.fulfill, + onUpdateSourceConfig: updateSourceConfigExpectation.fulfill + ) + destinationPlugin.client = client + client.addPlugin(destinationPlugin) + + client.track("test_track", properties: nil, option: nil) + client.screen("test_screen", category: nil, properties: nil, option: nil) + client.identify("test_user_id", traits: nil, option: nil) + client.alias("test_new_id", option: nil) + client.group("test_group_id", traits: nil, option: nil) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: false + ) + )) + + wait(for: [processMessageExpectation, updateSourceConfigExpectation], timeout: 1.0) + } + + func test_replayQueue_withCachedSourceConfigEnabledTrue_andNewSourceConfigEnabledFalse() { + let processMessageExpectation = expectation(description: "Process 5 messages") + processMessageExpectation.expectedFulfillmentCount = 5 + + let updateSourceConfigExpectation = expectation(description: "Update SourceConfig 2 times") + updateSourceConfigExpectation.expectedFulfillmentCount = 2 + + let client = RSClientMock() + + let replayQueuePlugin = ReplayQueuePlugin(queue: DispatchQueue(label: "replayQueueTests_1".queueLabel())) + replayQueuePlugin.client = client + client.addPlugin(replayQueuePlugin) + + let destinationPlugin = ReplayQueueTestDestination( + onProcessMessage: processMessageExpectation.fulfill, + onUpdateSourceConfig: updateSourceConfigExpectation.fulfill + ) + destinationPlugin.client = client + client.addPlugin(destinationPlugin) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: true + ) + )) + + client.track("test_track", properties: nil, option: nil) + client.screen("test_screen", category: nil, properties: nil, option: nil) + client.identify("test_user_id", traits: nil, option: nil) + client.alias("test_new_id", option: nil) + client.group("test_group_id", traits: nil, option: nil) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: false + ) + )) + + client.track("test_track_2", properties: nil, option: nil) + client.screen("test_screen_2", category: nil, properties: nil, option: nil) + client.identify("test_user_id_2", traits: nil, option: nil) + client.alias("test_new_id_2", option: nil) + client.group("test_group_id_2", traits: nil, option: nil) + + wait(for: [processMessageExpectation, updateSourceConfigExpectation], timeout: 1.0) + } + + func test_replayQueue_withCachedSourceConfigEnabledTrue_andNewSourceConfigEnabledTrue() { + let processMessageExpectation = expectation(description: "Process 5 messages") + processMessageExpectation.expectedFulfillmentCount = 10 + + let updateSourceConfigExpectation = expectation(description: "Update SourceConfig 2 times") + updateSourceConfigExpectation.expectedFulfillmentCount = 2 + + let client = RSClientMock() + + let replayQueuePlugin = ReplayQueuePlugin(queue: DispatchQueue(label: "replayQueueTests_1".queueLabel())) + replayQueuePlugin.client = client + client.addPlugin(replayQueuePlugin) + + let destinationPlugin = ReplayQueueTestDestination( + onProcessMessage: processMessageExpectation.fulfill, + onUpdateSourceConfig: updateSourceConfigExpectation.fulfill + ) + destinationPlugin.client = client + client.addPlugin(destinationPlugin) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: true + ) + )) + + client.track("test_track", properties: nil, option: nil) + client.screen("test_screen", category: nil, properties: nil, option: nil) + client.identify("test_user_id", traits: nil, option: nil) + client.alias("test_new_id", option: nil) + client.group("test_group_id", traits: nil, option: nil) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: true + ) + )) + + client.track("test_track_2", properties: nil, option: nil) + client.screen("test_screen_2", category: nil, properties: nil, option: nil) + client.identify("test_user_id_2", traits: nil, option: nil) + client.alias("test_new_id_2", option: nil) + client.group("test_group_id_2", traits: nil, option: nil) + + wait(for: [processMessageExpectation, updateSourceConfigExpectation], timeout: 1.0) + } + + func test_replayQueue_withCachedSourceConfigEnabledFalse_andNewSourceConfigEnabledTrue() { + let processMessageExpectation = expectation(description: "Process 5 messages") + processMessageExpectation.expectedFulfillmentCount = 5 + + let updateSourceConfigExpectation = expectation(description: "Update SourceConfig 2 times") + updateSourceConfigExpectation.expectedFulfillmentCount = 2 + + let client = RSClientMock() + + let replayQueuePlugin = ReplayQueuePlugin(queue: DispatchQueue(label: "replayQueueTests_1".queueLabel())) + replayQueuePlugin.client = client + client.addPlugin(replayQueuePlugin) + + let destinationPlugin = ReplayQueueTestDestination( + onProcessMessage: processMessageExpectation.fulfill, + onUpdateSourceConfig: updateSourceConfigExpectation.fulfill + ) + destinationPlugin.client = client + client.addPlugin(destinationPlugin) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: false + ) + )) + + client.track("test_track", properties: nil, option: nil) + client.screen("test_screen", category: nil, properties: nil, option: nil) + client.identify("test_user_id", traits: nil, option: nil) + client.alias("test_new_id", option: nil) + client.group("test_group_id", traits: nil, option: nil) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: true + ) + )) + + wait(for: [processMessageExpectation, updateSourceConfigExpectation], timeout: 1.0) + } + + func test_replayQueue_withCachedSourceConfigEnabledFalse_andNewSourceConfigEnabledFalse() { + let processMessageExpectation = expectation(description: "Process 5 messages") + processMessageExpectation.expectedFulfillmentCount = 5 + processMessageExpectation.isInverted = true + + let updateSourceConfigExpectation = expectation(description: "Update SourceConfig 2 times") + updateSourceConfigExpectation.expectedFulfillmentCount = 2 + + let client = RSClientMock() + + let replayQueuePlugin = ReplayQueuePlugin(queue: DispatchQueue(label: "replayQueueTests_1".queueLabel())) + replayQueuePlugin.client = client + client.addPlugin(replayQueuePlugin) + + let destinationPlugin = ReplayQueueTestDestination( + onProcessMessage: processMessageExpectation.fulfill, + onUpdateSourceConfig: updateSourceConfigExpectation.fulfill + ) + destinationPlugin.client = client + client.addPlugin(destinationPlugin) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: false + ) + )) + + client.track("test_track", properties: nil, option: nil) + client.screen("test_screen", category: nil, properties: nil, option: nil) + client.identify("test_user_id", traits: nil, option: nil) + client.alias("test_new_id", option: nil) + client.group("test_group_id", traits: nil, option: nil) + + client.updateSourceConfig(.mockWith( + source: .mockWith( + enabled: false + ) + )) + + wait(for: [processMessageExpectation, updateSourceConfigExpectation], timeout: 1.0) + } +} + +class ReplayQueueTestDestination: DestinationPlugin { + var name: String = "test_destination" + var plugins: [Rudder.Plugin] = [] + var type: Rudder.PluginType = .destination + var client: Rudder.RSClientProtocol? + var sourceConfig: Rudder.SourceConfig? { + didSet { + onUpdateSourceConfig?() + } + } + + var onProcessMessage: (() -> Void)? + var onUpdateSourceConfig: (() -> Void)? + + init(onProcessMessage: (() -> Void)? = nil, onUpdateSourceConfig: (() -> Void)? = nil) { + self.onProcessMessage = onProcessMessage + self.onUpdateSourceConfig = onUpdateSourceConfig + } + + func track(message: TrackMessage) -> TrackMessage? { + onProcessMessage?() + return message + } + + func screen(message: ScreenMessage) -> ScreenMessage? { + onProcessMessage?() + return message + } + + func alias(message: AliasMessage) -> AliasMessage? { + onProcessMessage?() + return message + } + + func identify(message: IdentifyMessage) -> IdentifyMessage? { + onProcessMessage?() + return message + } + + func group(message: GroupMessage) -> GroupMessage? { + onProcessMessage?() + return message + } +} diff --git a/RudderTests/Core/Plugins/UserSessionPluginTests.swift b/RudderTests/Core/Plugins/UserSessionPluginTests.swift new file mode 100644 index 00000000..56bfdfc2 --- /dev/null +++ b/RudderTests/Core/Plugins/UserSessionPluginTests.swift @@ -0,0 +1,311 @@ +// +// UserSessionPluginTests.swift +// Rudder +// +// Created by Pallab Maiti on 05/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + +final class UserSessionPluginTests: XCTestCase { + let userDefaultsWorker = UserDefaultsWorker( + suiteName: #file, + queue: DispatchQueue(label: "userSessionPluginTests".queueLabel()) + ) + + func test_sessionId() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // When + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: true, + autoSessionTracking: true + ), + userDefaultsWorker: userDefaultsWorker + ) + + // Then + XCTAssertNotNil(userSessionPlugin.sessionId) + } + + func test_startSession_trackLifecycleEvents_false() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // Given + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: false + ), + userDefaultsWorker: userDefaultsWorker + ) + + let trackMessage = userSessionPlugin.process(message: TrackMessage(event: "test_session")) + + XCTAssertNil(userSessionPlugin.sessionId) + XCTAssertNil(trackMessage?.sessionId) + + // When + userSessionPlugin.startSession() + + let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) + + // Then + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_2?.sessionId) + XCTAssertTrue(trackMessage_2?.sessionStart ?? false) + } + + func test_startSession_withID_trackLifecycleEvents_false() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // Given + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: false + ), + userDefaultsWorker: userDefaultsWorker + ) + + let trackMessage = userSessionPlugin.process(message: TrackMessage(event: "test_session")) + + XCTAssertNil(userSessionPlugin.sessionId) + XCTAssertNil(trackMessage?.sessionId) + + // When + let sessionId = 1234567890 + userSessionPlugin.startSession(sessionId) + + let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) + + // Then + XCTAssertEqual(userSessionPlugin.sessionId, sessionId) + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_2?.sessionId) + XCTAssertEqual(trackMessage_2?.sessionId, sessionId) + XCTAssertTrue(trackMessage_2?.sessionStart ?? false) + } + + func test_startSession_trackLifecycleEvents_true() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // Given + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: true, + autoSessionTracking: true + ), + userDefaultsWorker: userDefaultsWorker + ) + + let trackMessage = userSessionPlugin.process(message: TrackMessage(event: "test_session")) + + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage?.sessionId) + XCTAssertTrue(trackMessage?.sessionStart ?? false) + + let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) + + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_2?.sessionId) + XCTAssertFalse(trackMessage_2?.sessionStart ?? false) + XCTAssertEqual(trackMessage?.sessionId, trackMessage_2?.sessionId) + + usleep(2000000) + + // When + userSessionPlugin.startSession() + + let trackMessage_3 = userSessionPlugin.process(message: TrackMessage(event: "test_session_3")) + + // Then + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_3?.sessionId) + XCTAssertTrue(trackMessage_3?.sessionStart ?? false) + XCTAssertNotEqual(trackMessage?.sessionId, trackMessage_3?.sessionId) + XCTAssertNotEqual(trackMessage_2?.sessionId, trackMessage_3?.sessionId) + + let trackMessage_4 = userSessionPlugin.process(message: TrackMessage(event: "test_session_4")) + + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_4?.sessionId) + XCTAssertFalse(trackMessage_2?.sessionStart ?? false) + XCTAssertEqual(trackMessage_3?.sessionId, trackMessage_4?.sessionId) + XCTAssertNotEqual(trackMessage?.sessionId, trackMessage_4?.sessionId) + XCTAssertNotEqual(trackMessage_2?.sessionId, trackMessage_4?.sessionId) + } + + func test_startSession_withID_trackLifecycleEvents_true() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // Given + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: true, + autoSessionTracking: true + ), + userDefaultsWorker: userDefaultsWorker + ) + + let trackMessage = userSessionPlugin.process(message: TrackMessage(event: "test_session")) + + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage?.sessionId) + XCTAssertTrue(trackMessage?.sessionStart ?? false) + + let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) + + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_2?.sessionId) + XCTAssertFalse(trackMessage_2?.sessionStart ?? false) + XCTAssertEqual(trackMessage?.sessionId, trackMessage_2?.sessionId) + + // When + let sessionId = 1234567890 + userSessionPlugin.startSession(sessionId) + + let trackMessage_3 = userSessionPlugin.process(message: TrackMessage(event: "test_session_3")) + + // Then + XCTAssertEqual(userSessionPlugin.sessionId, sessionId) + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_3?.sessionId) + XCTAssertTrue(trackMessage_3?.sessionStart ?? false) + XCTAssertNotEqual(trackMessage?.sessionId, trackMessage_3?.sessionId) + XCTAssertNotEqual(trackMessage_2?.sessionId, trackMessage_3?.sessionId) + + let trackMessage_4 = userSessionPlugin.process(message: TrackMessage(event: "test_session_4")) + + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_4?.sessionId) + XCTAssertFalse(trackMessage_2?.sessionStart ?? false) + XCTAssertEqual(trackMessage_3?.sessionId, trackMessage_4?.sessionId) + XCTAssertNotEqual(trackMessage?.sessionId, trackMessage_4?.sessionId) + XCTAssertNotEqual(trackMessage_2?.sessionId, trackMessage_4?.sessionId) + } + + func test_endSession() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // Given + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: false, + autoSessionTracking: false + ), + userDefaultsWorker: userDefaultsWorker + ) + + let trackMessage = userSessionPlugin.process(message: TrackMessage(event: "test_session")) + + XCTAssertNil(trackMessage?.sessionId) + + userSessionPlugin.startSession() + + let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) + + XCTAssertEqual(userSessionPlugin.sessionId, trackMessage_2?.sessionId) + XCTAssertTrue(trackMessage_2?.sessionStart ?? false) + + // When + userSessionPlugin.endSession() + + let trackMessage_3 = userSessionPlugin.process(message: TrackMessage(event: "test_session_3")) + + // Then + XCTAssertNil(trackMessage_3?.sessionId) + } + + func test_refreshSessionIfNeeded() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // Given + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: true, + autoSessionTracking: true, + sessionTimeOut: 2000 + ), + userDefaultsWorker: userDefaultsWorker + ) + + let trackMessage = userSessionPlugin.process(message: TrackMessage(event: "test_session")) + + XCTAssertNotNil(trackMessage?.sessionId) + XCTAssertTrue(trackMessage?.sessionStart ?? false) + + usleep(2000000) + + // When + userSessionPlugin.refreshSessionIfNeeded() + + let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) + + // Then + XCTAssertNotEqual(trackMessage?.sessionId, trackMessage_2?.sessionId) + XCTAssertTrue(trackMessage_2?.sessionStart ?? false) + } + + func test_reset_AutomaticSessionTracking() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // Given + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: true, + autoSessionTracking: true + ), + userDefaultsWorker: userDefaultsWorker + ) + + let trackMessage = userSessionPlugin.process(message: TrackMessage(event: "test_session")) + + XCTAssertNotNil(trackMessage?.sessionId) + XCTAssertTrue(trackMessage?.sessionStart ?? false) + + // When + usleep(2000000) + userSessionPlugin.reset() + + let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) + + // Then + XCTAssertNotEqual(trackMessage?.sessionId, trackMessage_2?.sessionId) + XCTAssertTrue(trackMessage_2?.sessionStart ?? false) + } + + func test_reset_ManualSessionTracking() { + userDefaultsWorker.userDefaults?.removePersistentDomain(forName: #file) + + // Given + let userSessionPlugin = UserSessionPlugin() + userSessionPlugin.client = RSClientMock( + configuration: .mockWith( + trackLifecycleEvents: false, + autoSessionTracking: false + ), + userDefaultsWorker: userDefaultsWorker + ) + + let trackMessage = userSessionPlugin.process(message: TrackMessage(event: "test_session")) + + XCTAssertNil(trackMessage?.sessionId) + + userSessionPlugin.startSession() + + let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) + XCTAssertNotNil(trackMessage_2?.sessionId) + XCTAssertTrue(trackMessage_2?.sessionStart ?? false) + + usleep(2000000) + + // When + userSessionPlugin.reset() + + let trackMessage_3 = userSessionPlugin.process(message: TrackMessage(event: "test_session_3")) + + // Then + XCTAssertNotEqual(trackMessage?.sessionId, trackMessage_3?.sessionId) + XCTAssertTrue(trackMessage_3?.sessionStart ?? false) + } +} diff --git a/RudderTests/Core/SourceConfigDownload/SourceConfigDownloadTests.swift b/RudderTests/Core/SourceConfigDownload/SourceConfigDownloadTests.swift index f9ffbd45..50cb55df 100644 --- a/RudderTests/Core/SourceConfigDownload/SourceConfigDownloadTests.swift +++ b/RudderTests/Core/SourceConfigDownload/SourceConfigDownloadTests.swift @@ -64,7 +64,7 @@ final class SourceConfigDownloadTests: XCTestCase { // Then var count = 0 - download.sourceConfig = { sourceConfig in + download.sourceConfig = { sourceConfig, _ in expectation.fulfill() if count == 0 { XCTAssertEqual(cachedSourceConfig, sourceConfig) @@ -116,7 +116,7 @@ final class SourceConfigDownloadTests: XCTestCase { let download = SourceConfigDownload(downloader: worker) // Then - download.sourceConfig = { sourceConfig in + download.sourceConfig = { sourceConfig, _ in expectation.fulfill() XCTAssertEqual(downloadedSourceConfig, sourceConfig) } diff --git a/RudderTests/Core/SourceConfigDownload/SourceConfigDownloadWorkerTests.swift b/RudderTests/Core/SourceConfigDownload/SourceConfigDownloadWorkerTests.swift index c57b711f..ad9a920b 100644 --- a/RudderTests/Core/SourceConfigDownload/SourceConfigDownloadWorkerTests.swift +++ b/RudderTests/Core/SourceConfigDownload/SourceConfigDownloadWorkerTests.swift @@ -53,7 +53,7 @@ final class SourceConfigDownloadWorkerTests: XCTestCase { retryStrategy: retryStrategy ) - worker.sourceConfig = { expectedSourceConfig in + worker.sourceConfig = { expectedSourceConfig, _ in XCTAssertEqual(expectedSourceConfig, sourceConfig) } @@ -96,7 +96,7 @@ final class SourceConfigDownloadWorkerTests: XCTestCase { retryStrategy: retryStrategy ) - worker.sourceConfig = { _ in } + worker.sourceConfig = { _, _ in } wait(for: [expectation], timeout: 6.0) } diff --git a/RudderTests/Core/Storage/SQLiteStorageTests.swift b/RudderTests/Core/Storage/SQLiteStorageTests.swift new file mode 100644 index 00000000..aac9ef69 --- /dev/null +++ b/RudderTests/Core/Storage/SQLiteStorageTests.swift @@ -0,0 +1,118 @@ +// +// SQLiteStorageTests.swift +// RudderTests-iOS +// +// Created by Pallab Maiti on 04/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + + +final class SQLiteStorageTests: XCTestCase { + + var sqliteStorage: SQLiteStorage! + + override func setUp() { + super.setUp() + sqliteStorage = .mockWith(name: "sqlite_storage_test") + sqliteStorage.open() + sqliteStorage.deleteAll() + } + + func test_saveMessage() throws { + let entityList = getMessageEntity(by: 1) + // Given + entityList.forEach({ sqliteStorage.save($0) }) + + // When + let message = try sqliteStorage.objects(limit: 1).get().first?.message + + // Then + XCTAssertEqual(message, getMessage(index: 1)) + sqliteStorage.deleteAll() + } + + func test_fetchMessages() throws { + let entityList = getMessageEntity(by: 3) + + // Given + entityList.forEach({ sqliteStorage.save($0) }) + + // When + let messageList1 = try sqliteStorage.objects(limit: 5).get() + let messageList2 = try sqliteStorage.objects(limit: 2).get() + + // Then + XCTAssertEqual(messageList1.count, 3) + XCTAssertEqual(messageList2.count, 2) + sqliteStorage.deleteAll() + } + + func test_clearMessages() throws { + let entityList = getMessageEntity(by: 3) + + // Given + entityList.forEach({ sqliteStorage.save($0) }) + + let list = try sqliteStorage.objects(limit: 3).get() + + // When + sqliteStorage.delete(list) + + // Then + XCTAssertEqual(try sqliteStorage.count().get(), 0) + sqliteStorage.deleteAll() + } + + func test_getMessageCount() throws { + let entityList = getMessageEntity(by: 3) + + // Given + entityList.forEach({ sqliteStorage.save($0) }) + + // When + let count = try sqliteStorage.count().get() + + // Then + XCTAssertEqual(count, 3) + sqliteStorage.deleteAll() + } + + func test_clearAll() { + let entityList = getMessageEntity(by: 3) + + // Given + entityList.forEach({ sqliteStorage.save($0) }) + + // When + sqliteStorage.deleteAll() + + // Then + XCTAssertEqual(try sqliteStorage.count().get(), 0) + } + + func getMessage(index: Int) -> String { + return + """ + { + "key_\(index)": "value_\(index)" + } + """ + } + + func getMessageEntity(by count: Int) -> [StorageMessage] { + var entityList = [StorageMessage]() + for i in 1...count { + let message = getMessage(index: i) + entityList.append(StorageMessage(id: UUID().uuidString, message: message, updated: 1234567890)) + } + return entityList + } + + override func tearDown() { + super.tearDown() + sqliteStorage = nil + } +} diff --git a/RudderTests/Core/Storage/StorageMigratorTests.swift b/RudderTests/Core/Storage/StorageMigratorTests.swift new file mode 100644 index 00000000..b169e0f7 --- /dev/null +++ b/RudderTests/Core/Storage/StorageMigratorTests.swift @@ -0,0 +1,60 @@ +// +// StorageMigratorTests.swift +// RudderTests-iOS +// +// Created by Pallab Maiti on 02/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + +final class StorageMigratorTests: XCTestCase { + + func test_migrate() throws { + // Given + let path = FileManager.default.urls(for: .cachesDirectory, in: FileManager.SearchPathDomainMask.userDomainMask)[0] + let oldDatabase = SQLiteDatabase(path: path, name: "rl_persistence_test.sqlite") + let oldSQLiteStorage = SQLiteStorage( + database: oldDatabase, + logger: Logger(logger: NOLogger()) + ) + + oldSQLiteStorage.open() + oldSQLiteStorage.deleteAll() + + let databasePath = path.appendingPathComponent("rl_persistence_test.sqlite").path + XCTAssertTrue(FileManager.default.fileExists(atPath: databasePath)) + + oldSQLiteStorage.save(StorageMessage(id: "", message: "message_3", updated: 1234567890)) + oldSQLiteStorage.save(StorageMessage(id: "", message: "message_4", updated: 1235454094)) + oldSQLiteStorage.save(StorageMessage(id: "", message: "message_5", updated: 1245935445)) + oldSQLiteStorage.save(StorageMessage(id: "", message: "message_6", updated: 1223465723)) + + let currentDatabase = SQLiteDatabase(path: path, name: "rl_persistence_default_test.sqlite") + let currentStorage = SQLiteStorage( + database: currentDatabase, + logger: Logger(logger: NOLogger()) + ) + + currentStorage.open() + currentStorage.deleteAll() + + currentStorage.save(StorageMessage(id: "", message: "message_1", updated: 1246573777)) + currentStorage.save(StorageMessage(id: "", message: "message_2", updated: 1223546723)) + + XCTAssertEqual(try currentStorage.count().get(), 2) + + let storageMigrator = StorageMigratorV1V2(oldSQLiteStorage: oldSQLiteStorage, currentStorage: currentStorage) + + // When + try storageMigrator.migrate() + + // Then + XCTAssertEqual(try currentStorage.count().get(), 6) + XCTAssertFalse(FileManager.default.fileExists(atPath: databasePath)) + + oldSQLiteStorage.close() + currentStorage.close() + } +} diff --git a/RudderTests/Core/Storage/StorageTests.swift b/RudderTests/Core/Storage/StorageWorkerTests.swift similarity index 85% rename from RudderTests/Core/Storage/StorageTests.swift rename to RudderTests/Core/Storage/StorageWorkerTests.swift index aecec492..9f2dead3 100644 --- a/RudderTests/Core/Storage/StorageTests.swift +++ b/RudderTests/Core/Storage/StorageWorkerTests.swift @@ -9,20 +9,14 @@ import XCTest @testable import Rudder -class StorageTests: XCTestCase { +class StorageWorkerTests: XCTestCase { - var storageWorker: StorageWorker! + var storageWorker: StorageWorkerProtocol! override func setUp() { super.setUp() - let path = FileManager.default.urls(for: .cachesDirectory, in: FileManager.SearchPathDomainMask.userDomainMask)[0] - let database = DefaultDatabase(path: path, name: "rl_persistence.sqlite") - let storage = DefaultStorage( - database: database, - logger: Logger(logger: NOLogger()) - ) - storageWorker = DefaultStorageWorker( - storage: storage, + storageWorker = StorageWorker( + storage: StorageMock(), queue: DispatchQueue(label: "testStorageWorker".queueLabel()) ) storageWorker.open() @@ -114,7 +108,7 @@ class StorageTests: XCTestCase { var entityList = [StorageMessage]() for i in 1...count { let message = getMessage(index: i) - entityList.append(StorageMessage(id: UUID().uuidString, message: message)) + entityList.append(StorageMessage(id: UUID().uuidString, message: message, updated: 1234567890)) } return entityList } diff --git a/RudderTests/Mocks/APIClientMock.swift b/RudderTests/Mocks/APIClientMock.swift index 275df08d..b310b74c 100644 --- a/RudderTests/Mocks/APIClientMock.swift +++ b/RudderTests/Mocks/APIClientMock.swift @@ -1,6 +1,6 @@ // // APIClientMock.swift -// Rudder +// RudderStackTests // // Created by Pallab Maiti on 25/01/24. // Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. diff --git a/RudderTests/Mocks/ConsoleLoggerMock.swift b/RudderTests/Mocks/ConsoleLoggerMock.swift new file mode 100644 index 00000000..71c666fd --- /dev/null +++ b/RudderTests/Mocks/ConsoleLoggerMock.swift @@ -0,0 +1,27 @@ +// +// ConsoleLoggerMock.swift +// Rudder +// +// Created by Pallab Maiti on 07/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation +import Rudder + +class ConsoleLoggerMock: LoggerProtocol { + let logLevel: LogLevel + var logMessage: String = "" + + init(logLevel: LogLevel) { + self.logLevel = logLevel + } + + func log(_ message: String, logLevel: Rudder.LogLevel, file: String, function: String, line: Int) { + if self.logLevel == .verbose || self.logLevel == logLevel { + logMessage = message + } else { + logMessage = "" + } + } +} diff --git a/RudderTests/Mocks/CoreMocks.swift b/RudderTests/Mocks/CoreMocks.swift index 425bb737..818dd836 100644 --- a/RudderTests/Mocks/CoreMocks.swift +++ b/RudderTests/Mocks/CoreMocks.swift @@ -1,6 +1,6 @@ // // CoreMocks.swift -// Rudder +// RudderStackTests // // Created by Pallab Maiti on 24/01/24. // Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. @@ -9,8 +9,8 @@ import Foundation @testable import Rudder -extension Config { - static func mockAny() -> Config { +extension Configuration { + static func mockAny() -> Configuration { .mockWith() } @@ -25,13 +25,14 @@ extension Config { recordScreenViews: Bool = .random(), controlPlaneURL: String = .mockAnyURL(), autoSessionTracking: Bool = .random(), - sessionTimeout: Int = .mockRandom(), + sessionTimeOut: Int = .mockRandom(), gzipEnabled: Bool = .random(), dataResidencyServer: DataResidencyServer = .US, flushPolicies: [FlushPolicy] = [FlushPolicy](), dataUploadRetryPolicy: RetryPolicy? = nil, - sourceConfigDownloadRetryPolicy: RetryPolicy? = nil - ) -> Config { + sourceConfigDownloadRetryPolicy: RetryPolicy? = nil, + logger: LoggerProtocol? = NOLogger() + ) -> Configuration { .init(writeKey: writeKey, dataPlaneURL: dataPlaneURL)! .flushQueueSize(flushQueueSize) .dbCountThreshold(dbCountThreshold) @@ -41,12 +42,13 @@ extension Config { .recordScreenViews(recordScreenViews) .controlPlaneURL(controlPlaneURL) .autoSessionTracking(autoSessionTracking) - .sessionTimeout(sessionTimeout) + .sessionTimeOut(sessionTimeOut) .gzipEnabled(gzipEnabled) .dataResidencyServer(dataResidencyServer) .flushPolicies(flushPolicies) .dataUploadRetryPolicy(dataUploadRetryPolicy) .sourceConfigDownloadRetryPolicy(sourceConfigDownloadRetryPolicy) + .logger(logger) } } @@ -199,13 +201,14 @@ extension StorageMessage { static func mockWith( id: String = .mockRandom(among: .alphanumerics, length: 32), - message: String = .mockRandom(length: 20) + message: String = .mockRandom(length: 20), + updated: Int = .mockRandom(max: 10) ) -> Self { - .init(id: id, message: message) + .init(id: id, message: message, updated: updated) } } -class UserDefaultsWorkerMock: UserDefaultsWorkerType { +class UserDefaultsWorkerMock: UserDefaultsWorkerProtocol { var value: Codable? let queue: DispatchQueue @@ -301,3 +304,55 @@ class ServerMock { return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) } } + +extension SQLiteStorage { + static func mockAny() -> Self { + .mockWith() + } + + static func mockWith( + path: URL = FileManager.default.urls(for: .cachesDirectory, in: FileManager.SearchPathDomainMask.userDomainMask)[0], + name: String = .mockRandom(), + logger: LoggerProtocol = NOLogger() + ) -> Self { + let path = path + let database = SQLiteDatabase(path: path, name: "\(name).sqlite") + return self.init(database: database, logger: Logger(logger: logger)) + } +} + +extension RSClient { + static func mockAny() -> RSClient { + .mockWith() + } + + static func mockWith( + configuration: Configuration = .mockAny(), + instanceName: String = ClientRegistry.defaultInstanceName, + database: Database? = SQLiteDatabaseMock(), + storage: Storage? = StorageMock(), + userDefaults: UserDefaults? = UserDefaults(suiteName: #file), + apiClient: APIClient? = URLSessionClient(session: .shared), + sourceConfigDownloader: SourceConfigDownloaderType? = SourceConfigDownloaderMock(downloadStatus: .mockWith(responseCode: 200)), + dataUploader: DataUploaderType? = DataUploaderMock(uploadStatus: .mockWith(responseCode: 200)) + ) -> RSClient { + return initialize( + with: configuration, + instanceName: instanceName, + database: database, + storage: storage, + userDefaults: userDefaults, + apiClient: apiClient, + sourceConfigDownloader: sourceConfigDownloader, + dataUploader: dataUploader + ) + } +} + +class StorageMigratorMock: StorageMigrator { + var currentStorage: Storage = StorageMock() + + func migrate() throws { + + } +} diff --git a/RudderTests/Mocks/NOLogger.swift b/RudderTests/Mocks/NOLogger.swift index 7a92360b..65c3254a 100644 --- a/RudderTests/Mocks/NOLogger.swift +++ b/RudderTests/Mocks/NOLogger.swift @@ -1,30 +1,16 @@ // // NOLogger.swift -// Rudder +// RudderStackTests // // Created by Pallab Maiti on 22/01/24. // Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. // import Foundation -import Rudder +@testable import Rudder class NOLogger: LoggerProtocol { - func logDebug(_ message: String, file: String, function: String, line: Int) { + func log(_ message: String, logLevel: Rudder.LogLevel, file: String, function: String, line: Int) { } - - func logInfo(_ message: String, file: String, function: String, line: Int) { - - } - - func logWarning(_ message: String, file: String, function: String, line: Int) { - - } - - func logError(_ message: String, file: String, function: String, line: Int) { - - } - - } diff --git a/RudderTests/Mocks/PrintFunctionMock.swift b/RudderTests/Mocks/PrintFunctionMock.swift new file mode 100644 index 00000000..6197b189 --- /dev/null +++ b/RudderTests/Mocks/PrintFunctionMock.swift @@ -0,0 +1,24 @@ +// +// PrintFunctionMock.swift +// Rudder +// +// Created by Pallab Maiti on 07/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +class PrintFunctionMock { + var printedMessages: [String] = [] + var printedMessage: String? { printedMessages.last } + + init() { } + + func print(message: String) { + printedMessages.append(message) + } + + func reset() { + printedMessages = [] + } +} diff --git a/RudderTests/Mocks/RSClientMock.swift b/RudderTests/Mocks/RSClientMock.swift new file mode 100644 index 00000000..9a429477 --- /dev/null +++ b/RudderTests/Mocks/RSClientMock.swift @@ -0,0 +1,180 @@ +// +// RSClientMock.swift +// Rudder +// +// Created by Pallab Maiti on 06/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation +@testable import Rudder + +class RSClientMock: RSClientProtocol { + func track(_ eventName: String, properties: Rudder.TrackProperties?, option: Rudder.MessageOption?) { + let message = TrackMessage(event: eventName, properties: properties, option: option) + process(message: message) + } + + func identify(_ userId: String, traits: Rudder.IdentifyTraits?, option: Rudder.IdentifyOptionType?) { + let message = IdentifyMessage(userId: userId, traits: traits, option: option) + process(message: message) + } + + func screen(_ screenName: String, category: String?, properties: Rudder.ScreenProperties?, option: Rudder.MessageOption?) { + let message = ScreenMessage(title: screenName, category: category, properties: properties, option: option) + process(message: message) + } + + func group(_ groupId: String, traits: Rudder.GroupTraits?, option: Rudder.MessageOption?) { + let message = GroupMessage(groupId: groupId, traits: traits, option: option) + process(message: message) + } + + func alias(_ newId: String, option: Rudder.MessageOption?) { + let message = AliasMessage(newId: newId, option: option) + process(message: message) + } + + func process(message: Message) { + switch message { + case let e as TrackMessage: + process(e) + case let e as IdentifyMessage: + process(e) + case let e as ScreenMessage: + process(e) + case let e as GroupMessage: + process(e) + case let e as AliasMessage: + process(e) + default: + break + } + + @discardableResult + func process(_ message: T) -> T? { + let defaultMesage = register(message: message, for: .default) + register(message: defaultMesage, for: .destination) + + @discardableResult + func register(message: T?, for pluginType: PluginType) -> T? { + if let list = pluginList[pluginType], let msg = message { + return list.process(message: msg) + } + return message + } + return defaultMesage + } + } + + var instanceName: String + var configuration: Rudder.Configuration + var userDefaultsWorker: Rudder.UserDefaultsWorkerProtocol + var storageWorker: Rudder.StorageWorkerProtocol + var sessionStorage: Rudder.SessionStorageProtocol + let logger = Logger(logger: NOLogger()) + + @ReadWriteLock var pluginList: [PluginType: [Plugin]] = [ + .default: [Plugin](), + .destination: [Plugin]() + ] + + init( + instanceName: String = "dead", + configuration: Rudder.Configuration = .mockAny(), + userDefaultsWorker: Rudder.UserDefaultsWorkerProtocol = UserDefaultsWorkerMock( + queue: DispatchQueue( + label: "clientMock".queueLabel() + ) + ), + storageWorker: Rudder.StorageWorkerProtocol = StorageWorkerMock(), + sessionStorage: Rudder.SessionStorageProtocol = SessionStorageMock() + ) { + self.instanceName = instanceName + self.configuration = configuration + self.userDefaultsWorker = userDefaultsWorker + self.storageWorker = storageWorker + self.sessionStorage = sessionStorage + } + + func logDebug(_ message: String, file: String, function: String, line: Int) { + logger.logDebug(LogMessages.customMessage(message), file: file, function: function, line: line) + } + + func logInfo(_ message: String, file: String, function: String, line: Int) { + logger.logInfo(LogMessages.customMessage(message), file: file, function: function, line: line) + } + + func logWarning(_ message: String, file: String, function: String, line: Int) { + logger.logWarning(LogMessages.customMessage(message), file: file, function: function, line: line) + } + + func logError(_ message: String, file: String, function: String, line: Int) { + logger.logError(LogMessages.customMessage(message), file: file, function: function, line: line) + } + + func addPlugin(_ plugin: Rudder.Plugin) { + if var list = pluginList[plugin.type] { + list.addPlugin(plugin) + pluginList[plugin.type] = list + } + } + + func removePlugin(_ plugin: Rudder.Plugin) { + PluginType.allCases.forEach { pluginType in + if var pluginList = pluginList[pluginType] { + let removeList = pluginList.filter({ $0 === plugin }) + removeList.forEach({ pluginList.removePlugin($0) }) + } + } + } + + func getAllPlugins() -> [Plugin]? { + return pluginList.flatMap { (_, value) in + return value + } + } + + func getDestinationPlugins() -> [DestinationPlugin]? { + return getPluginList(by: .destination) as? [DestinationPlugin] + } + + func getDefaultPlugins() -> [Plugin]? { + return getPluginList(by: .default) + } + + func getPluginList(by pluginType: Rudder.PluginType) -> [Rudder.Plugin]? { + return pluginList[pluginType] + } + + func getPlugin(type: T.Type) -> T? where T: Rudder.Plugin { + var filteredList = [Plugin]() + PluginType.allCases.forEach { pluginType in + if let pluginList = pluginList[pluginType] { + filteredList.append(contentsOf: pluginList.filter({ $0 is T })) + } + } + return filteredList.first as? T + } + + func associatePlugins(_ handler: (Rudder.Plugin) -> Void) { + PluginType.allCases.forEach { pluginType in + if let pluginList = pluginList[pluginType] { + pluginList.forEach { plugin in + handler(plugin) + if let plugin = plugin as? DestinationPlugin { + plugin.associate(handler: handler) + } + } + } + } + } + + func updateSourceConfig(_ sourceConfig: SourceConfig) { + pluginList.forEach { (_, value) in + value.forEach { plugin in + plugin.sourceConfig = sourceConfig + } + } + } +} diff --git a/RudderTests/Mocks/SQLiteDatabaseMock.swift b/RudderTests/Mocks/SQLiteDatabaseMock.swift new file mode 100644 index 00000000..8da5c5c6 --- /dev/null +++ b/RudderTests/Mocks/SQLiteDatabaseMock.swift @@ -0,0 +1,75 @@ +// +// SQLiteDatabaseMock.swift +// Rudder +// +// Created by Pallab Maiti on 05/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation +@testable import Rudder + +class SQLiteDatabaseMock: Database { + var path: URL = .mockAny() + + var name: String = .mockAny() + + func open() -> Int32 { + return 0 + } + + func prepare(_ zSql: UnsafePointer!, _ nByte: Int32, _ ppStmt: UnsafeMutablePointer!, _ pzTail: UnsafeMutablePointer?>!) -> Int32 { + return 0 + } + + func step(_ pStmt: OpaquePointer!) -> Int32 { + return 0 + } + + func finalize(_ pStmt: OpaquePointer!) -> Int32 { + return 0 + } + + func bind_text(_ pStmt: OpaquePointer!, _ i: Int32, _ zData: UnsafePointer!, _ nData: Int32, _ enc: (@convention(c) (UnsafeMutableRawPointer?) -> Void)!) -> Int32 { + return 0 + } + + func bind_int(_ p: OpaquePointer!, _ i: Int32, _ iValue: Int32) -> Int32 { + return 0 + } + + func errmsg() -> UnsafePointer! { + return NSString(string: "").utf8String + } + + func column_int(_: OpaquePointer!, _ iCol: Int32) -> Int32 { + return 0 + } + + func column_text(_: OpaquePointer!, _ iCol: Int32) -> UnsafePointer! { + return "".toPointer() + } + + func exec(_ sql: UnsafePointer!, _ callback: (@convention(c) (UnsafeMutableRawPointer?, Int32, UnsafeMutablePointer?>?, UnsafeMutablePointer?>?) -> Int32)!, _ arg: UnsafeMutableRawPointer!, _ errmsg: UnsafeMutablePointer?>!) -> Int32 { + return 0 + } + + func close() -> Int32 { + return 0 + } +} + +extension String { + func toPointer() -> UnsafePointer? { + guard let data = self.data(using: String.Encoding.utf8) else { return nil } + let buffer = UnsafeMutablePointer.allocate(capacity: data.count) + let stream = OutputStream(toBuffer: buffer, capacity: data.count) + stream.open() + data.withUnsafeBytes{ dataBytes in + let buffer: UnsafePointer = dataBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + stream.write(buffer, maxLength: dataBytes.count) + } + stream.close() + return UnsafePointer(buffer) + } +} diff --git a/RudderTests/Mocks/SessionStorageMock.swift b/RudderTests/Mocks/SessionStorageMock.swift new file mode 100644 index 00000000..168a0995 --- /dev/null +++ b/RudderTests/Mocks/SessionStorageMock.swift @@ -0,0 +1,22 @@ +// +// SessionStorageMock.swift +// Rudder +// +// Created by Pallab Maiti on 06/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation +@testable import Rudder + +class SessionStorageMock: SessionStorageProtocol { + var value: Any? + + func write(_ key: SessionStorageKeys, value: T?) { + self.value = value + } + + func read(_ key: SessionStorageKeys) -> T? { + return value as? T + } +} diff --git a/RudderTests/Mocks/SourceConfigMock.swift b/RudderTests/Mocks/SourceConfigMock.swift index f241834d..a51ddbc2 100644 --- a/RudderTests/Mocks/SourceConfigMock.swift +++ b/RudderTests/Mocks/SourceConfigMock.swift @@ -1,6 +1,6 @@ // // SourceConfigMock.swift -// Rudder +// RudderStackTests // // Created by Pallab Maiti on 25/01/24. // Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. diff --git a/RudderTests/Mocks/StorageMock.swift b/RudderTests/Mocks/StorageMock.swift index 58b0e6ae..072f2246 100644 --- a/RudderTests/Mocks/StorageMock.swift +++ b/RudderTests/Mocks/StorageMock.swift @@ -1,6 +1,6 @@ // // StorageMock.swift -// Rudder +// RudderStackTests // // Created by Pallab Maiti on 22/01/24. // Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. @@ -12,11 +12,13 @@ import Rudder class StorageMock: Storage { var messageList = [StorageMessage]() + @discardableResult func open() -> Rudder.Results { messageList = [StorageMessage]() return .success(true) } + @discardableResult func save(_ object: Rudder.StorageMessage) -> Rudder.Results { messageList.append(object) return .success(true) @@ -29,6 +31,7 @@ class StorageMock: Storage { return .success(messageList) } + @discardableResult func delete(_ objects: [Rudder.StorageMessage]) -> Rudder.Results { objects.forEach { message in messageList.removeAll(where: { $0.id == message.id }) @@ -36,12 +39,53 @@ class StorageMock: Storage { return .success(true) } + @discardableResult func deleteAll() -> Rudder.Results { messageList.removeAll() return .success(true) } + @discardableResult func count() -> Rudder.Results { return .success(messageList.count) } + + @discardableResult + func close() -> Results { + return .success(true) + } +} + +class StorageWorkerMock: StorageWorkerProtocol { + let storage = StorageMock() + + func open() { + storage.open() + } + + func saveMessage(_ message: Rudder.StorageMessage) { + storage.save(message) + } + + func clearMessages(_ messages: [Rudder.StorageMessage]) { + storage.delete(messages) + } + + func fetchMessages(limit: Int) -> [Rudder.StorageMessage]? { + try? storage.objects(limit: limit).get() + } + + func getMessageCount() -> Int? { + try? storage.count().get() + } + + func clearAll() { + storage.deleteAll() + } + + func close() { + storage.close() + } + + } diff --git a/Sources/Classes/Core/ApplicationState/MacApplicationState.swift b/Sources/Classes/Core/ApplicationState/MacApplicationState.swift index ebd1e382..02bfc393 100644 --- a/Sources/Classes/Core/ApplicationState/MacApplicationState.swift +++ b/Sources/Classes/Core/ApplicationState/MacApplicationState.swift @@ -14,7 +14,7 @@ import Cocoa class MacApplicationState: ApplicationStateProtocol { let application: NSApplication - let userDefaults: UserDefaultsWorkerType + let userDefaults: UserDefaultsWorkerProtocol var trackApplicationStateMessage: ((ApplicationStateMessage) -> Void) = { _ in } var refreshSessionIfNeeded: (() -> Void) = { } @@ -22,7 +22,7 @@ class MacApplicationState: ApplicationStateProtocol { @ReadWriteLock private var didFinishLaunching = false @ReadWriteLock private var fromBackground = false - init(application: NSApplication, userDefaults: UserDefaultsWorkerType) { + init(application: NSApplication, userDefaults: UserDefaultsWorkerProtocol) { self.application = application self.userDefaults = userDefaults } @@ -144,7 +144,7 @@ extension ApplicationState { static func current( notificationCenter: NotificationCenter, application: NSApplication = NSApplication.shared, - userDefaults: UserDefaultsWorkerType, + userDefaults: UserDefaultsWorkerProtocol, notifications: [Notification.Name] = [ NSApplication.didFinishLaunchingNotification, NSApplication.didResignActiveNotification, diff --git a/Sources/Classes/Core/ApplicationState/PhoneApplicationState.swift b/Sources/Classes/Core/ApplicationState/PhoneApplicationState.swift index 977be1ff..0fd80171 100644 --- a/Sources/Classes/Core/ApplicationState/PhoneApplicationState.swift +++ b/Sources/Classes/Core/ApplicationState/PhoneApplicationState.swift @@ -14,7 +14,7 @@ import UIKit class PhoneApplicationState: ApplicationStateProtocol { let application: UIApplication - let userDefaults: UserDefaultsWorkerType + let userDefaults: UserDefaultsWorkerProtocol var trackApplicationStateMessage: ((ApplicationStateMessage) -> Void) = { _ in } var refreshSessionIfNeeded: (() -> Void) = { } @@ -22,7 +22,7 @@ class PhoneApplicationState: ApplicationStateProtocol { @ReadWriteLock private var isFirstTimeLaunch: Bool = true @ReadWriteLock private var didFinishLaunching = false - init(application: UIApplication, userDefaults: UserDefaultsWorkerType) { + init(application: UIApplication, userDefaults: UserDefaultsWorkerProtocol) { self.application = application self.userDefaults = userDefaults } @@ -124,7 +124,7 @@ extension ApplicationState { static func current( notificationCenter: NotificationCenter, application: UIApplication = UIApplication.shared, - userDefaults: UserDefaultsWorkerType, + userDefaults: UserDefaultsWorkerProtocol, notifications: [Notification.Name] = [ UIApplication.didEnterBackgroundNotification, UIApplication.willEnterForegroundNotification, diff --git a/Sources/Classes/Core/ApplicationState/WatchApplicationState.swift b/Sources/Classes/Core/ApplicationState/WatchApplicationState.swift index 743e8474..0651fae1 100644 --- a/Sources/Classes/Core/ApplicationState/WatchApplicationState.swift +++ b/Sources/Classes/Core/ApplicationState/WatchApplicationState.swift @@ -14,12 +14,12 @@ import WatchKit class WatchApplicationState: ApplicationStateProtocol { let wkExtension: WKExtension - let userDefaults: UserDefaultsWorkerType + let userDefaults: UserDefaultsWorkerProtocol var trackApplicationStateMessage: ((ApplicationStateMessage) -> Void) = { _ in } var refreshSessionIfNeeded: (() -> Void) = { } - init(wkExtension: WKExtension, userDefaults: UserDefaultsWorkerType) { + init(wkExtension: WKExtension, userDefaults: UserDefaultsWorkerProtocol) { self.wkExtension = wkExtension self.userDefaults = userDefaults } @@ -102,7 +102,7 @@ extension ApplicationState { static func current( notificationCenter: NotificationCenter, wkExtension: WKExtension = WKExtension.shared(), - userDefaults: UserDefaultsWorkerType, + userDefaults: UserDefaultsWorkerProtocol, notifications: [Notification.Name] = [ WKExtension.applicationDidFinishLaunchingNotification, WKExtension.applicationWillEnterForegroundNotification, diff --git a/Sources/Classes/Core/ClientRegistry.swift b/Sources/Classes/Core/ClientRegistry.swift new file mode 100644 index 00000000..7fcf6cc1 --- /dev/null +++ b/Sources/Classes/Core/ClientRegistry.swift @@ -0,0 +1,48 @@ +// +// ClientRegistry.swift +// Rudder +// +// Created by Pallab Maiti on 31/01/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +/// A registry for all instances. +public class ClientRegistry { + + @ReadWriteLock + static var instances: [String: RSClient] = [:] + + /// The name for the default instance. + public static let defaultInstanceName = "default" + + private init() { } + + static var `default`: RSClient? { + instances[defaultInstanceName] + } + + static func register(_ instance: RSClient, name: String) { + guard !isRegistered(instanceName: name) else { + return + } + instances[name] = instance + } + + static func isRegistered(instanceName: String) -> Bool { + return instances[instanceName] != nil + } + + static func instance(named name: String) -> RSClient? { + instances[name] + } + + static func unregisterInstance(named name: String) { + instances.removeValue(forKey: name) + } + + static func unregisterDefault() { + unregisterInstance(named: defaultInstanceName) + } +} diff --git a/Sources/Classes/Core/Common/Constants/LogMessages.swift b/Sources/Classes/Core/Common/Constants/LogMessages.swift index 88fc11c2..f5b3f4bc 100644 --- a/Sources/Classes/Core/Common/Constants/LogMessages.swift +++ b/Sources/Classes/Core/Common/Constants/LogMessages.swift @@ -9,6 +9,19 @@ import Foundation enum LogMessages { + enum API { + case flush + case sourceConfig + + var description: String { + switch self { + case .flush: + return "Aborting flush" + case .sourceConfig: + return "Server config download failed" + } + } + } case optOut case optOutAndEventDrop case tokenNotEmpty @@ -23,10 +36,6 @@ enum LogMessages { case eventNameNotEmpty case sourceConfigDownloadSuccess case eventsCleared - case flushAbortedWithStatusCode(Int) - case flushAbortedWithErrorDescription(String) - case sourceConfigDownloadFailedWithStatusCode(Int) - case sourceConfigDownloadFailedWithErrorDescription(String) case newSession case sessionCanNotStart case sessionIdLengthInvalid(Int) @@ -45,7 +54,10 @@ enum LogMessages { case retryAborted(String, Int) case destinationDisabled case eventFiltered - case noResponse + case storageMigrationFailed(StorageError) + case storageMigrationSuccess + case oldDatabaseNotExists + case apiError(API, APIError) var description: String { switch self { @@ -77,14 +89,6 @@ enum LogMessages { return "Source config download successful" case .eventsCleared: return "Clearing events from storage" - case .flushAbortedWithStatusCode(let statusCode): - return "Aborting flush. Error code: \(statusCode)" - case .flushAbortedWithErrorDescription(let errorDescription): - return "Aborting flush. Error: \(errorDescription)" - case .sourceConfigDownloadFailedWithStatusCode(let statusCode): - return "Server config download failed. Error code: \(statusCode)" - case .sourceConfigDownloadFailedWithErrorDescription(let errorDescription): - return "Server config download failed. Error: \(errorDescription)" case .newSession: return "New session is started" case .sessionCanNotStart: @@ -96,7 +100,7 @@ enum LogMessages { case .sqlStatement(let statement): return "SQL: \(statement)" case .statementNotPrepared(let errorDescription): - return "Statement is not prepared, reason: \(errorDescription)" + return "Statement is not prepared. Reason: \(errorDescription)" case .eventInsertionSuccess: return "Event inserted" case .eventInsertionFailure: @@ -121,8 +125,21 @@ enum LogMessages { return "Destination is not enabled" case .eventFiltered: return "Message is filtered by Client-side event filtering" - case .noResponse: - return "No server response" + case .storageMigrationFailed(let error): + return "Storage migration failed. Reason: \(error.description)" + case .storageMigrationSuccess: + return "Storage migration is successful" + case .oldDatabaseNotExists: + return "Old database not exists, hence no migration needed" + case .apiError(let api, let error): + switch error { + case .httpError(let statusCode): + return "\(api.description). Error code: \(statusCode)" + case .networkError(let error): + return "\(api.description). Error: \(error.localizedDescription)" + case .noResponse: + return "No server response" + } } } } diff --git a/Sources/Classes/Core/DataUpload/DataUploadWorker.swift b/Sources/Classes/Core/DataUpload/DataUploadWorker.swift index 44d781a2..3a62ef3e 100644 --- a/Sources/Classes/Core/DataUpload/DataUploadWorker.swift +++ b/Sources/Classes/Core/DataUpload/DataUploadWorker.swift @@ -16,8 +16,8 @@ protocol DataUploadWorkerType { class DataUploadWorker: DataUploadWorkerType { let dataUploader: DataUploaderType let dataUploadBlockers: DownloadUploadBlockersProtocol - let storageWorker: StorageWorker - let config: Config + let storageWorker: StorageWorkerProtocol + let config: Configuration let queue: DispatchQueue let logger: Logger let retryStrategy: DownloadUploadRetryStrategy @@ -31,8 +31,8 @@ class DataUploadWorker: DataUploadWorkerType { init( dataUploader: DataUploaderType, dataUploadBlockers: DownloadUploadBlockersProtocol, - storageWorker: StorageWorker, - config: Config, + storageWorker: StorageWorkerProtocol, + config: Configuration, queue: DispatchQueue, logger: Logger, retryStrategy: DownloadUploadRetryStrategy @@ -76,14 +76,7 @@ class DataUploadWorker: DataUploadWorkerType { self.storageWorker.clearMessages(messageList) } if let error = uploadStatus.error { - switch error { - case .httpError(let statusCode): - self.logger.logError(.flushAbortedWithStatusCode(statusCode)) - case .networkError(let error): - self.logger.logError(.flushAbortedWithErrorDescription(error.localizedDescription)) - case .noResponse: - self.logger.logError(.noResponse) - } + self.logger.logError(.apiError(.flush, error)) } self.flushNextBatch() } @@ -101,15 +94,17 @@ class DataUploadWorker: DataUploadWorkerType { func flushSynchronously() { queue.sync { [weak self] in guard let self = self, - let messageList = storageWorker.fetchMessages(limit: config.dbCountThreshold), - let batches = getBatches(messageList: messageList) else { + let messageList = self.storageWorker.fetchMessages(limit: self.config.dbCountThreshold), + let batches = self.getBatches(messageList: messageList) else { return } for batch in batches { - defer { + let uploadStatus = self.dataUploader.upload(messages: batch.messages) + if let error = uploadStatus.error { + self.logger.logDebug(.apiError(.flush, error)) + } else { self.storageWorker.clearMessages(batch.messages) } - _ = self.dataUploader.upload(messages: batch.messages) } } } diff --git a/Sources/Classes/Core/Domain/Models/Config.swift b/Sources/Classes/Core/Domain/Models/Configuration.swift similarity index 73% rename from Sources/Classes/Core/Domain/Models/Config.swift rename to Sources/Classes/Core/Domain/Models/Configuration.swift index 9b997920..a6f09c6d 100644 --- a/Sources/Classes/Core/Domain/Models/Config.swift +++ b/Sources/Classes/Core/Domain/Models/Configuration.swift @@ -13,15 +13,30 @@ public enum DataResidencyServer { case EU } -public enum ConfigError: Error { +enum ConfigValidationError: Error { case flushQueueSize case dbCountThreshold case sleepTimeOut case controlPlaneURL - case sessionTimeout + case sessionTimeOut + + var description: String { + switch self { + case .flushQueueSize: + return "flushQueueSize is out of range. Min: 1, Max: 100. Set to default" + case .dbCountThreshold: + return "dbCountThreshold is invalid. Min: 1. Set to default" + case .sleepTimeOut: + return "sleepTimeOut is invalid. Min: 10. Set to default" + case .controlPlaneURL: + return "controlPlaneURL is invalid" + case .sessionTimeOut: + return "sessionTimeout is invalid. Min: 0. Set to default" + } + } } -public class Config { +public class Configuration { let _writeKey: String public var writeKey: String { return _writeKey @@ -72,9 +87,9 @@ public class Config { return _autoSessionTracking } - private var _sessionTimeout: Int = Constants.sessionTimeOut.default - public var sessionTimeout: Int { - return _sessionTimeout + private var _sessionTimeOut: Int = Constants.sessionTimeOut.default + public var sessionTimeOut: Int { + return _sessionTimeOut } private var _gzipEnabled: Bool = Constants.gzipEnabled.default @@ -102,6 +117,13 @@ public class Config { _sourceConfigDownloadRetryPolicy } + private var _logger: LoggerProtocol? + public var logger: LoggerProtocol? { + _logger + } + + var configValidationErrorList = [ConfigValidationError]() + required public init?(writeKey: String, dataPlaneURL: String) { guard writeKey.isNotEmpty, let url = URL(string: dataPlaneURL), url.isValid else { return nil @@ -111,9 +133,9 @@ public class Config { } @discardableResult - public func flushQueueSize(_ flushQueueSize: Int) -> Config { + public func flushQueueSize(_ flushQueueSize: Int) -> Configuration { guard flushQueueSize >= Constants.queueSize.min && flushQueueSize <= Constants.queueSize.max else { -// Logger.logError("flushQueueSize is out of range. Min: 1, Max: 100. Set to default") + configValidationErrorList.append(.flushQueueSize) return self } _flushQueueSize = flushQueueSize @@ -121,15 +143,15 @@ public class Config { } @discardableResult - public func logLevel(_ logLevel: LogLevel) -> Config { + public func logLevel(_ logLevel: LogLevel) -> Configuration { _logLevel = logLevel return self } @discardableResult - public func dbCountThreshold(_ dbCountThreshold: Int) -> Config { + public func dbCountThreshold(_ dbCountThreshold: Int) -> Configuration { guard dbCountThreshold >= Constants.storageCountThreshold.min else { -// Logger.logError("dbCountThreshold is invalid. Min: 1. Set to default") + configValidationErrorList.append(.dbCountThreshold) return self } _dbCountThreshold = dbCountThreshold @@ -137,9 +159,9 @@ public class Config { } @discardableResult - public func sleepTimeOut(_ sleepTimeOut: Int) -> Config { + public func sleepTimeOut(_ sleepTimeOut: Int) -> Configuration { guard sleepTimeOut >= Constants.sleepTimeOut.min else { -// Logger.logError("sleepTimeOut is invalid. Min: 10. Set to default") + configValidationErrorList.append(.sleepTimeOut) return self } _sleepTimeOut = sleepTimeOut @@ -147,21 +169,21 @@ public class Config { } @discardableResult - public func trackLifecycleEvents(_ trackLifecycleEvents: Bool) -> Config { + public func trackLifecycleEvents(_ trackLifecycleEvents: Bool) -> Configuration { _trackLifecycleEvents = trackLifecycleEvents return self } @discardableResult - public func recordScreenViews(_ recordScreenViews: Bool) -> Config { + public func recordScreenViews(_ recordScreenViews: Bool) -> Configuration { _recordScreenViews = recordScreenViews return self } @discardableResult - public func controlPlaneURL(_ controlPlaneURL: String) -> Config { + public func controlPlaneURL(_ controlPlaneURL: String) -> Configuration { guard let url = URL(string: controlPlaneURL), url.isValid else { -// Logger.logError("controlPlaneURL is invalid") + configValidationErrorList.append(.controlPlaneURL) return self } _controlPlaneURL = url.absoluteString.rectified @@ -169,35 +191,35 @@ public class Config { } @discardableResult - public func autoSessionTracking(_ autoSessionTracking: Bool) -> Config { + public func autoSessionTracking(_ autoSessionTracking: Bool) -> Configuration { _autoSessionTracking = autoSessionTracking return self } @discardableResult - public func sessionTimeout(_ sessionTimeout: Int) -> Config { - guard sessionTimeout >= Constants.sessionTimeOut.min else { -// Logger.logError("sessionTimeout is invalid. Min: 0. Set to default") + public func sessionTimeOut(_ sessionTimeOut: Int) -> Configuration { + guard sessionTimeOut >= Constants.sessionTimeOut.min else { + configValidationErrorList.append(.sessionTimeOut) return self } - _sessionTimeout = sessionTimeout + _sessionTimeOut = sessionTimeOut return self } @discardableResult - public func gzipEnabled(_ gzipEnabled: Bool) -> Config { + public func gzipEnabled(_ gzipEnabled: Bool) -> Configuration { _gzipEnabled = gzipEnabled return self } @discardableResult - public func dataResidencyServer(_ dataResidencyServer: DataResidencyServer) -> Config { + public func dataResidencyServer(_ dataResidencyServer: DataResidencyServer) -> Configuration { _dataResidencyServer = dataResidencyServer return self } @discardableResult - public func flushPolicies(_ flushPolicies: [FlushPolicy]) -> Config { + public func flushPolicies(_ flushPolicies: [FlushPolicy]) -> Configuration { if !flushPolicies.isEmpty { _flushPolicies.append(contentsOf: flushPolicies) } @@ -205,16 +227,22 @@ public class Config { } @discardableResult - public func dataUploadRetryPolicy(_ dataUploadRetryPolicy: RetryPolicy?) -> Config { + public func dataUploadRetryPolicy(_ dataUploadRetryPolicy: RetryPolicy?) -> Configuration { _dataUploadRetryPolicy = dataUploadRetryPolicy return self } @discardableResult - public func sourceConfigDownloadRetryPolicy(_ sourceConfigDownloadRetryPolicy: RetryPolicy?) -> Config { + public func sourceConfigDownloadRetryPolicy(_ sourceConfigDownloadRetryPolicy: RetryPolicy?) -> Configuration { _sourceConfigDownloadRetryPolicy = sourceConfigDownloadRetryPolicy return self } + + @discardableResult + public func logger(_ logger: LoggerProtocol?) -> Configuration { + _logger = logger + return self + } } extension URL { diff --git a/Sources/Classes/Core/Domain/Models/Context.swift b/Sources/Classes/Core/Domain/Models/Context.swift index 13d3a55b..f802120b 100644 --- a/Sources/Classes/Core/Domain/Models/Context.swift +++ b/Sources/Classes/Core/Domain/Models/Context.swift @@ -172,7 +172,7 @@ public struct Context: Codable { return TimeZone.current.identifier } - static func traits(userDefaults: UserDefaultsWorkerType?) -> JSON? { + static func traits(userDefaults: UserDefaultsWorkerProtocol?) -> JSON? { let traitsJSON: JSON? = userDefaults?.read(.traits) var traitsDict = traitsJSON?.dictionaryValue if let userId: String = userDefaults?.read(.userId) { @@ -187,7 +187,7 @@ public struct Context: Codable { return nil } - internal init(userDefaults: UserDefaultsWorkerType?) { + internal init(userDefaults: UserDefaultsWorkerProtocol?) { _app = AppInfo() _device = DeviceInfo() _library = LibraryInfo() diff --git a/Sources/Classes/Core/Domain/Models/StorageMessage.swift b/Sources/Classes/Core/Domain/Models/StorageMessage.swift index 8cad0065..0a44500c 100644 --- a/Sources/Classes/Core/Domain/Models/StorageMessage.swift +++ b/Sources/Classes/Core/Domain/Models/StorageMessage.swift @@ -11,10 +11,12 @@ import Foundation public struct StorageMessage { public let id: String public let message: String + public let updated: Int - init(id: String, message: String) { + public init(id: String, message: String, updated: Int) { self.id = id self.message = message + self.updated = updated } } diff --git a/Sources/Classes/Core/Helpers/Logger.swift b/Sources/Classes/Core/Helpers/Logger.swift deleted file mode 100644 index 40cbc694..00000000 --- a/Sources/Classes/Core/Helpers/Logger.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// Logger.swift -// Rudder -// -// Created by Pallab Maiti on 27/11/23. -// Copyright © 2023 Rudder Labs India Pvt Ltd. All rights reserved. -// - -import Foundation - -@frozen -@objc(RSLogLevel) -public enum LogLevel: Int { - case verbose = 5 - case debug = 4 - case info = 3 - case warning = 2 - case error = 1 - case none = 0 - - public func toString() -> String { - switch self { - case .verbose: - return "Verbose" - case .debug: - return "Debug" - case .info: - return "Info" - case .warning: - return "Warning" - case .error: - return "Error" - case .none: - return "" - } - } -} - -public protocol LoggerProtocol { - func logDebug(_ message: String, file: String, function: String, line: Int) - func logInfo(_ message: String, file: String, function: String, line: Int) - func logWarning(_ message: String, file: String, function: String, line: Int) - func logError(_ message: String, file: String, function: String, line: Int) -} - -class Logger { - let logger: LoggerProtocol - - init(logger: LoggerProtocol) { - self.logger = logger - } - - func logDebug(_ message: LogMessages, file: String = #file, function: String = #function, line: Int = #line) { - logger.logDebug(message.description, file: file, function: function, line: line) - } - - func logInfo(_ message: LogMessages, file: String = #file, function: String = #function, line: Int = #line) { - logger.logInfo(message.description, file: file, function: function, line: line) - } - - func logWarning(_ message: LogMessages, file: String = #file, function: String = #function, line: Int = #line) { - logger.logWarning(message.description, file: file, function: function, line: line) - } - - func logError(_ message: LogMessages, file: String = #file, function: String = #function, line: Int = #line) { - logger.logError(message.description, file: file, function: function, line: line) - } -} - -class ConsoleLogger: LoggerProtocol { - let logLevel: LogLevel - - init(logLevel: LogLevel = .error) { - self.logLevel = logLevel - } - - func logDebug(_ message: String, file: String, function: String, line: Int) { - log(message: message, logLevel: .debug, file: file, function: function, line: line) - } - - func logInfo(_ message: String, file: String, function: String, line: Int) { - log(message: message, logLevel: .info, file: file, function: function, line: line) - } - - func logWarning(_ message: String, file: String, function: String, line: Int) { - log(message: message, logLevel: .warning, file: file, function: function, line: line) - } - - func logError(_ message: String, file: String, function: String, line: Int) { - log(message: message, logLevel: .error, file: file, function: function, line: line) - } - - private func log(message: String, logLevel: LogLevel, file: String = #file, function: String = #function, line: Int = #line) { - if logLevel == .verbose || logLevel == self.logLevel { - let metadata = " - \(((file as NSString).lastPathComponent as NSString).deletingPathExtension):\(function):\(line):" - print("RudderStack:\(logLevel.toString()):\(metadata)\(message)") - } - } -} - -extension RSClient: LoggerProtocol { - public func logDebug(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - controller.logger.logDebug(LogMessages.customMessage(message), file: file, function: function, line: line) - } - - public func logInfo(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - controller.logger.logInfo(LogMessages.customMessage(message), file: file, function: function, line: line) - } - - public func logWarning(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - controller.logger.logWarning(LogMessages.customMessage(message), file: file, function: function, line: line) - } - - public func logError(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - controller.logger.logError(LogMessages.customMessage(message), file: file, function: function, line: line) - } -} diff --git a/Sources/Classes/Core/Helpers/SessionStorage.swift b/Sources/Classes/Core/Helpers/SessionStorage.swift index 0c264f97..c44557a9 100644 --- a/Sources/Classes/Core/Helpers/SessionStorage.swift +++ b/Sources/Classes/Core/Helpers/SessionStorage.swift @@ -8,22 +8,27 @@ import Foundation -class SessionStorage { - enum Keys: String { - case deviceToken - case advertisingId - case appTrackingConsent - case defaultOption - case context - } - +public enum SessionStorageKeys: String { + case deviceToken + case advertisingId + case appTrackingConsent + case defaultOption + case context +} + +public protocol SessionStorageProtocol { + func write(_ key: SessionStorageKeys, value: T?) + func read(_ key: SessionStorageKeys) -> T? +} + +class SessionStorage: SessionStorageProtocol { @ReadWriteLock private var deviceToken: String? @ReadWriteLock private var advertisingId: String? @ReadWriteLock private var appTrackingConsent: AppTrackingConsent? @ReadWriteLock private var defaultOption: Option? @ReadWriteLock private var context: Context? - - func write(_ key: SessionStorage.Keys, value: T?) { + + func write(_ key: SessionStorageKeys, value: T?) { switch key { case .deviceToken: deviceToken = value as? String @@ -40,7 +45,7 @@ class SessionStorage { } } - func read(_ key: SessionStorage.Keys) -> T? { + func read(_ key: SessionStorageKeys) -> T? { var result: T? switch key { case .deviceToken: diff --git a/Sources/Classes/Core/Helpers/UserDefaultsWorker.swift b/Sources/Classes/Core/Helpers/UserDefaultsWorker.swift index 3f45a968..f57ac85e 100644 --- a/Sources/Classes/Core/Helpers/UserDefaultsWorker.swift +++ b/Sources/Classes/Core/Helpers/UserDefaultsWorker.swift @@ -8,7 +8,7 @@ import Foundation -enum UserDefaultsKeys: String { +public enum UserDefaultsKeys: String { case userId = "rs_user_id" case traits = "rs_traits" case anonymousId = "rs_anonymous_id" @@ -26,13 +26,13 @@ enum UserDefaultsKeys: String { case build = "rs_application_build_key" } -protocol UserDefaultsWorkerType { +public protocol UserDefaultsWorkerProtocol { func write(_ key: UserDefaultsKeys, value: T?) func read(_ key: UserDefaultsKeys) -> T? func remove(_ key: UserDefaultsKeys) } -class UserDefaultsWorker: UserDefaultsWorkerType { +class UserDefaultsWorker: UserDefaultsWorkerProtocol { let queue: DispatchQueue let userDefaults: UserDefaults? diff --git a/Sources/Classes/Core/Helpers/Utility.swift b/Sources/Classes/Core/Helpers/Utility.swift index cb6edb66..b3eca258 100644 --- a/Sources/Classes/Core/Helpers/Utility.swift +++ b/Sources/Classes/Core/Helpers/Utility.swift @@ -30,18 +30,6 @@ class Utility { return NSUUID().uuidString.lowercased() } - static func getLocale() -> String { - let locale = Locale.current - if #available(iOS 10.0, *) { - return String(format: "%@-%@", locale.languageCode!, locale.regionCode!) - } - return "NA" - } - - static func getNumberOfBatch(from totalEventsCount: Int, and flushQueueSize: Int) -> Int { - return (totalEventsCount % flushQueueSize == 0) ? (totalEventsCount / flushQueueSize) : ((totalEventsCount / flushQueueSize) + 1) - } - static func getLifeCycleProperties(previousVersion: String? = nil, previousBuild: String? = nil, currentVersion: String? = nil, @@ -73,9 +61,4 @@ class Utility { } return properties } - - static func getPath(from directory: FileManager.SearchPathDirectory) -> URL { - return FileManager.default.urls(for: directory, in: FileManager.SearchPathDomainMask.userDomainMask)[0] - } - } diff --git a/Sources/Classes/Core/Logger/ConsoleLogger.swift b/Sources/Classes/Core/Logger/ConsoleLogger.swift new file mode 100644 index 00000000..d5c01ec7 --- /dev/null +++ b/Sources/Classes/Core/Logger/ConsoleLogger.swift @@ -0,0 +1,36 @@ +// +// ConsoleLogger.swift +// Rudder +// +// Created by Pallab Maiti on 06/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +public protocol LoggerProtocol { + func log(_ message: String, logLevel: LogLevel, file: String, function: String, line: Int) +} + +/// Function printing `String` content to console. +var consolePrint: (String) -> Void = { print($0) } + +class ConsoleLogger: LoggerProtocol { + private let logLevel: LogLevel + private let instanceName: String + private let printFunction: (String) -> Void + + init(logLevel: LogLevel, instanceName: String, printFunction: @escaping (String) -> Void = consolePrint) { + self.logLevel = logLevel + self.instanceName = instanceName + self.printFunction = printFunction + } + + func log(_ message: String, logLevel: LogLevel, file: String = #file, function: String = #function, line: Int = #line) { + if self.logLevel == .verbose || self.logLevel == logLevel { + let fileName = (file as NSString).lastPathComponent + let log = "[RUDDERSTACK SDK] - \(logLevel.tag) | [\(instanceName)] - \(fileName):\(function):\(line) | \(message)" + printFunction(log) + } + } +} diff --git a/Sources/Classes/Core/Logger/LogLevel.swift b/Sources/Classes/Core/Logger/LogLevel.swift new file mode 100644 index 00000000..901ad20d --- /dev/null +++ b/Sources/Classes/Core/Logger/LogLevel.swift @@ -0,0 +1,35 @@ +// +// LogLevel.swift +// Rudder +// +// Created by Pallab Maiti on 06/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +@frozen +@objc(RSLogLevel) +public enum LogLevel: Int { + case verbose = 5 + case debug = 4 + case info = 3 + case warning = 2 + case error = 1 + case none = 0 + + var tag: String { + switch self { + case .debug: + return "DEBUG" + case .info: + return "INFO" + case .warning: + return "WARN" + case .error: + return "ERROR" + default: + return "" + } + } +} diff --git a/Sources/Classes/Core/Logger/Logger.swift b/Sources/Classes/Core/Logger/Logger.swift new file mode 100644 index 00000000..917e5fcb --- /dev/null +++ b/Sources/Classes/Core/Logger/Logger.swift @@ -0,0 +1,51 @@ +// +// Logger.swift +// Rudder +// +// Created by Pallab Maiti on 27/11/23. +// Copyright © 2023 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +public class Logger { + let logger: LoggerProtocol + + init(logger: LoggerProtocol) { + self.logger = logger + } + + func logDebug(_ message: LogMessages, file: String = #file, function: String = #function, line: Int = #line) { + logger.log(message.description, logLevel: .debug, file: file, function: function, line: line) + } + + func logInfo(_ message: LogMessages, file: String = #file, function: String = #function, line: Int = #line) { + logger.log(message.description, logLevel: .info, file: file, function: function, line: line) + } + + func logWarning(_ message: LogMessages, file: String = #file, function: String = #function, line: Int = #line) { + logger.log(message.description, logLevel: .warning, file: file, function: function, line: line) + } + + func logError(_ message: LogMessages, file: String = #file, function: String = #function, line: Int = #line) { + logger.log(message.description, logLevel: .error, file: file, function: function, line: line) + } +} + +public extension Logger { + func logDebug(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + logger.log(message, logLevel: .debug, file: file, function: function, line: line) + } + + func logInfo(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + logger.log(message, logLevel: .info, file: file, function: function, line: line) + } + + func logWarning(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + logger.log(message, logLevel: .warning, file: file, function: function, line: line) + } + + func logError(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + logger.log(message, logLevel: .error, file: file, function: function, line: line) + } +} diff --git a/Sources/Classes/Core/Message.swift b/Sources/Classes/Core/Message.swift index 397ef686..b10473f9 100644 --- a/Sources/Classes/Core/Message.swift +++ b/Sources/Classes/Core/Message.swift @@ -58,7 +58,7 @@ public struct TrackMessage: Message { dictionary["properties"] = properties } - init(event: String, properties: TrackProperties?, option: MessageOptionType? = nil) { + init(event: String, properties: TrackProperties? = nil, option: MessageOptionType? = nil) { self.event = event self.properties = properties self.option = option @@ -194,7 +194,7 @@ public struct AliasMessage: Message { dictionary["previousId"] = previousId } - init(newId: String, previousId: String?, option: MessageOptionType? = nil) { + init(newId: String, previousId: String? = nil, option: MessageOptionType? = nil) { self.userId = newId self.previousId = previousId self.option = option diff --git a/Sources/Classes/Core/ObjC/ObjCRSClient.swift b/Sources/Classes/Core/ObjC/ObjCRSClient.swift new file mode 100644 index 00000000..e3ea35d1 --- /dev/null +++ b/Sources/Classes/Core/ObjC/ObjCRSClient.swift @@ -0,0 +1,81 @@ +// +// ObjCRSClient.swift +// Rudder +// +// Created by Pallab Maiti on 05/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +@objc(RSClient) +public class ObjCRSClient: NSObject { + /* + public func track(_ eventName: String, properties: TrackProperties) { + controller.track(eventName, properties: properties, option: nil) + } + + public func track(_ eventName: String, option: MessageOption) { + controller.track(eventName, properties: nil, option: option) + } + + public func track(_ eventName: String) { + controller.track(eventName, properties: nil, option: nil) + } + public func identify(_ userId: String, traits: IdentifyTraits) { + controller.identify(userId, traits: traits, option: nil) + } + + public func identify(_ userId: String, option: IdentifyOptionType) { + controller.identify(userId, traits: nil, option: option) + } + + public func identify(_ userId: String) { + controller.identify(userId, traits: nil, option: nil) + } + + public func screen(_ screenName: String, properties: ScreenProperties, option: MessageOption) { + controller.screen(screenName, category: nil, properties: properties, option: option) + } + + public func screen(_ screenName: String, properties: ScreenProperties) { + controller.screen(screenName, category: nil, properties: properties, option: nil) + } + + public func screen(_ screenName: String, option: MessageOption) { + controller.screen(screenName, category: nil, properties: nil, option: option) + } + + public func screen(_ screenName: String, category: String, properties: ScreenProperties) { + controller.screen(screenName, category: category, properties: properties, option: nil) + } + + public func screen(_ screenName: String, category: String, option: MessageOption) { + controller.screen(screenName, category: category, properties: nil, option: option) + } + + public func screen(_ screenName: String, category: String) { + controller.screen(screenName, category: category, properties: nil, option: nil) + } + + public func screen(_ screenName: String) { + controller.screen(screenName, category: nil, properties: nil, option: nil) + } + + public func group(_ groupId: String, traits: GroupTraits) { + controller.group(groupId, traits: traits, option: nil) + } + + public func group(_ groupId: String, option: MessageOption) { + controller.group(groupId, traits: nil, option: option) + } + + public func group(_ groupId: String) { + controller.group(groupId, traits: nil, option: nil) + } + + public func alias(_ newId: String) { + controller.alias(newId, option: nil) + } + */ +} diff --git a/Sources/Classes/Core/Plugins/ContextPlugin.swift b/Sources/Classes/Core/Plugins/ContextPlugin.swift index ba3d7428..84b780fb 100644 --- a/Sources/Classes/Core/Plugins/ContextPlugin.swift +++ b/Sources/Classes/Core/Plugins/ContextPlugin.swift @@ -11,7 +11,7 @@ import Foundation class ContextPlugin: Plugin { var type: PluginType = .default - var client: RSClient? { + var client: RSClientProtocol? { didSet { initialSetup() } @@ -21,11 +21,11 @@ class ContextPlugin: Plugin { private var staticContext = staticContextData() private static var device = Device.current - private var userDefaults: UserDefaultsWorkerType? + private var userDefaultsWorker: UserDefaultsWorkerProtocol? func initialSetup() { guard let client = self.client else { return } - userDefaults = client.controller.userDefaults + userDefaultsWorker = client.userDefaultsWorker } func process(message: T?) -> T? where T: Message { @@ -39,7 +39,7 @@ class ContextPlugin: Plugin { context.merge(eventContext) { (new, _) in new } } workingMessage.context = context - client?.controller.sessionStorage.write(.context, value: context) + client?.sessionStorage.write(.context, value: context) return workingMessage } @@ -72,14 +72,14 @@ class ContextPlugin: Plugin { } internal func insertDynamicDeviceInfoData(context: inout [String: Any]) { - if let deviceToken: String = client?.controller.sessionStorage.read(.deviceToken) { + if let deviceToken: String = client?.sessionStorage.read(.deviceToken) { context[keyPath: "device.token"] = deviceToken } - if let advertisingId: String = client?.controller.sessionStorage.read(.advertisingId), advertisingId.isNotEmpty { + if let advertisingId: String = client?.sessionStorage.read(.advertisingId), advertisingId.isNotEmpty { context[keyPath: "device.advertisingId"] = advertisingId context[keyPath: "device.adTrackingEnabled"] = true } - let appTrackingConsent: AppTrackingConsent = client?.controller.sessionStorage.read(.appTrackingConsent) ?? .notDetermined + let appTrackingConsent: AppTrackingConsent = client?.sessionStorage.read(.appTrackingConsent) ?? .notDetermined context[keyPath: "device.attTrackingStatus"] = appTrackingConsent.rawValue } @@ -87,7 +87,7 @@ class ContextPlugin: Plugin { // First priority will given to the `option` passed along with the event var contextExternalIds = [[String: String]]() // Fetch `externalIds` set using identify API. - if let externalIds: [[String: String]] = userDefaults?.read(.externalId) { + if let externalIds: [[String: String]] = userDefaultsWorker?.read(.externalId) { contextExternalIds.append(contentsOf: externalIds) } diff --git a/Sources/Classes/Core/Plugins/EventFiltering.swift b/Sources/Classes/Core/Plugins/EventFiltering.swift index 70d76945..1165b420 100644 --- a/Sources/Classes/Core/Plugins/EventFiltering.swift +++ b/Sources/Classes/Core/Plugins/EventFiltering.swift @@ -75,12 +75,12 @@ extension DestinationPlugin { private func filter(_ message: T) -> T? { guard isDestinationEnabled(message: message) else { - client?.logDebug(LogMessages.destinationDisabled.description) + client?.logger.logDebug(.destinationDisabled) return nil } guard shouldAllow(message: message) else { - client?.logDebug(LogMessages.eventFiltered.description) + client?.logger.logDebug(.eventFiltered) return nil } diff --git a/Sources/Classes/Core/Plugins/IntegrationPlugin.swift b/Sources/Classes/Core/Plugins/IntegrationPlugin.swift index da40cbd1..63f9f41b 100644 --- a/Sources/Classes/Core/Plugins/IntegrationPlugin.swift +++ b/Sources/Classes/Core/Plugins/IntegrationPlugin.swift @@ -12,12 +12,12 @@ class IntegrationPlugin: Plugin { var type: PluginType = .default var sourceConfig: SourceConfig? - var client: RSClient? + var client: RSClientProtocol? func process(message: T?) -> T? where T: Message { guard var workingMessage = message else { return message } let messageIntegrations = workingMessage.option?.integrations ?? [:] - let globalOption: Option? = client?.controller.sessionStorage.read(.defaultOption) + let globalOption: Option? = client?.sessionStorage.read(.defaultOption) let globalOptionIntegrations = globalOption?.integrations ?? [:] var integrations = messageIntegrations.merging(globalOptionIntegrations) { (current, _) in current } diff --git a/Sources/Classes/Core/Plugins/Plugins.swift b/Sources/Classes/Core/Plugins/Plugins.swift index ca768684..46a8910a 100644 --- a/Sources/Classes/Core/Plugins/Plugins.swift +++ b/Sources/Classes/Core/Plugins/Plugins.swift @@ -15,7 +15,7 @@ public enum PluginType: CaseIterable { public protocol Plugin: AnyObject { var type: PluginType { get set } - var client: RSClient? { get set } + var client: RSClientProtocol? { get set } var sourceConfig: SourceConfig? { get set } func process(message: T?) -> T? } diff --git a/Sources/Classes/Core/Plugins/ReplayQueuePlugin.swift b/Sources/Classes/Core/Plugins/ReplayQueuePlugin.swift index bc8b5aaa..7fd6b73d 100644 --- a/Sources/Classes/Core/Plugins/ReplayQueuePlugin.swift +++ b/Sources/Classes/Core/Plugins/ReplayQueuePlugin.swift @@ -13,19 +13,16 @@ internal class ReplayQueuePlugin: Plugin { var sourceConfig: SourceConfig? { didSet { - if oldValue == nil { - if let value = oldValue, value.enabled { - running = false - } + if let value = sourceConfig, value.enabled { + running = false + replayEvents() } else { - if let value = sourceConfig { - replayEvents(isSourceEnabled: value.enabled) - } + running = true } } } - var client: RSClient? + var client: RSClientProtocol? private let queue: DispatchQueue private var queuedMessageList = [Message]() @@ -38,52 +35,52 @@ internal class ReplayQueuePlugin: Plugin { } func process(message: T?) -> T? where T: Message { - if running, let msg = message { + guard let message = message else { return message } + if running { queue.async { [weak self] in guard let self = self else { return } if self.queuedMessageList.count >= self.maxSize { self.queuedMessageList.removeFirst() } - self.queuedMessageList.append(msg) + self.queuedMessageList.append(message) } + } else { + processToAllDestinations([message]) } return message } } extension ReplayQueuePlugin { - internal func replayEvents(isSourceEnabled: Bool) { + private func replayEvents() { queue.async { [weak self] in guard let self = self else { return } - self.running = false - guard isSourceEnabled else { - return - } - if let destinationPlugins = self.client?.controller.getPluginList(by: .destination) { - self.queuedMessageList.forEach { message in - destinationPlugins.forEach { plugin in - self.process(message, for: plugin) - } - } - } + self.processToAllDestinations(self.queuedMessageList) self.queuedMessageList.removeAll() } } - func process(_ message: Message, for plugin: Plugin) { - switch message { - case let e as TrackMessage: - _ = plugin.process(message: e) - case let e as IdentifyMessage: - _ = plugin.process(message: e) - case let e as ScreenMessage: - _ = plugin.process(message: e) - case let e as GroupMessage: - _ = plugin.process(message: e) - case let e as AliasMessage: - _ = plugin.process(message: e) - default: - break + private func processToAllDestinations(_ messageList: [Message]) { + guard let destinationPlugins = client?.getDestinationPlugins(), !destinationPlugins.isEmpty, !messageList.isEmpty else { + return + } + messageList.forEach { message in + destinationPlugins.forEach { plugin in + switch message { + case let e as TrackMessage: + _ = plugin.track(message: e) + case let e as IdentifyMessage: + _ = plugin.identify(message: e) + case let e as ScreenMessage: + _ = plugin.screen(message: e) + case let e as GroupMessage: + _ = plugin.group(message: e) + case let e as AliasMessage: + _ = plugin.alias(message: e) + default: + break + } + } } } } diff --git a/Sources/Classes/Core/Plugins/StoragePlugin.swift b/Sources/Classes/Core/Plugins/StoragePlugin.swift index 5f29bd63..5b590cd3 100644 --- a/Sources/Classes/Core/Plugins/StoragePlugin.swift +++ b/Sources/Classes/Core/Plugins/StoragePlugin.swift @@ -11,14 +11,14 @@ import Foundation class StoragePlugin: Plugin { var type: PluginType = .default - var client: RSClient? { + var client: RSClientProtocol? { didSet { - storageWorker = client?.controller.storageWorker + storageWorker = client?.storageWorker } } var sourceConfig: SourceConfig? - var storageWorker: StorageWorker? + var storageWorker: StorageWorkerProtocol? func process(message: T?) -> T? where T: Message { guard let message = message else { return message } @@ -26,9 +26,9 @@ class StoragePlugin: Plugin { let messageString = try message.toString.get() // we are assigning random UUID string // for DefaultDatabase we don't need the id as the table is auto incremented - storageWorker?.saveMessage(StorageMessage(id: UUID().uuidString, message: messageString)) + storageWorker?.saveMessage(StorageMessage(id: UUID().uuidString, message: messageString, updated: Utility.getTimeStamp())) } catch { - client?.logError(LogMessages.failedJSONConversion(error.localizedDescription).description) + client?.logger.logError(.failedJSONConversion(error.localizedDescription)) } return message } diff --git a/Sources/Classes/Core/Plugins/UserSessionPlugin.swift b/Sources/Classes/Core/Plugins/UserSessionPlugin.swift index a26ef729..1039b3aa 100644 --- a/Sources/Classes/Core/Plugins/UserSessionPlugin.swift +++ b/Sources/Classes/Core/Plugins/UserSessionPlugin.swift @@ -7,139 +7,183 @@ import Foundation -class UserSessionPlugin: Plugin { - var sourceConfig: SourceConfig? - - var type: PluginType = .default - - var client: RSClient? { - didSet { - setUp() - } - } - - private var userDefaults: UserDefaultsWorkerType? - private var sessionTimeOut: Int? - private var isNewSessionStarted = false +struct UserSessionPresets { + let userDefaultsWorker: UserDefaultsWorkerProtocol + let configuration: Configuration + + var isNewSessionStarted = false var sessionId: Int? { - userDefaults?.read(.sessionId) + get { + userDefaultsWorker.read(.sessionId) + } + set { + if newValue == nil { + userDefaultsWorker.remove(.sessionId) + } else { + userDefaultsWorker.write(.sessionId, value: newValue) + } + } } - private var lastEventTimeStamp: Int? { - userDefaults?.read(.lastEventTimeStamp) + var lastEventTimeStamp: Int? { + get { + userDefaultsWorker.read(.lastEventTimeStamp) + } + set { + if newValue == nil { + userDefaultsWorker.remove(.lastEventTimeStamp) + } else { + userDefaultsWorker.write(.lastEventTimeStamp, value: newValue) + } + } } - private var automaticSessionTrackingStatus: Bool { - userDefaults?.read(.automaticSessionTrackingStatus) ?? false + var automaticSessionTrackingStatus: Bool { + get { + userDefaultsWorker.read(.automaticSessionTrackingStatus) ?? false + } + set { + userDefaultsWorker.write(.automaticSessionTrackingStatus, value: newValue) + } } - private var sessionStoppedStatus: Bool { - userDefaults?.read(.sessionStoppedStatus) ?? false + var sessionStoppedStatus: Bool { + get { + userDefaultsWorker.read(.sessionStoppedStatus) ?? false + } + set { + userDefaultsWorker.write(.sessionStoppedStatus, value: newValue) + } } - private var manualSessionTrackingStatus: Bool { - userDefaults?.read(.manualSessionTrackingStatus) ?? false + var manualSessionTrackingStatus: Bool { + get { + userDefaultsWorker.read(.manualSessionTrackingStatus) ?? false + } + set { + userDefaultsWorker.write(.manualSessionTrackingStatus, value: newValue) + } } - private var isSessionTrackingAllowed: Bool { + var isSessionTrackingAllowed: Bool { if !sessionStoppedStatus && (manualSessionTrackingStatus || isAutomaticSessionTrackingAllowed) { return true } return false } - private var isAutomaticSessionTrackingAllowed: Bool { - guard let config = client?.config, config.trackLifecycleEvents, config.automaticSessionTracking else { - return false + var isAutomaticSessionTrackingAllowed: Bool { + return configuration.trackLifecycleEvents && configuration.automaticSessionTracking + } + + var isSessionExpired: Bool { + guard let lastEventTimeStamp = self.lastEventTimeStamp else { + return true } - return true + + let timeDifference: TimeInterval = TimeInterval(abs(Utility.getTimeStamp() - lastEventTimeStamp)) + return timeDifference >= Double(configuration.sessionTimeOut / 1000) } + init(userDefaultsWorker: UserDefaultsWorkerProtocol, configuration: Configuration) { + self.userDefaultsWorker = userDefaultsWorker + self.configuration = configuration + } +} + +class UserSessionPlugin: Plugin { + var sourceConfig: SourceConfig? + + var type: PluginType = .default + + var client: RSClientProtocol? { + didSet { + setUp() + } + } + + private var userSessionPresets: UserSessionPresets? + var sessionId: Int? { + userSessionPresets?.sessionId + } + func setUp() { guard let client = self.client else { return } - sessionTimeOut = client.config.sessionTimeout - userDefaults = client.controller.userDefaults - - if isAutomaticSessionTrackingAllowed { - if isSessionExpired() || !automaticSessionTrackingStatus { - startNewSession() - userDefaults?.write(.automaticSessionTrackingStatus, value: true) - userDefaults?.write(.manualSessionTrackingStatus, value: false) - userDefaults?.write(.sessionStoppedStatus, value: false) - } + userSessionPresets = UserSessionPresets(userDefaultsWorker: client.userDefaultsWorker, configuration: client.configuration) + if userSessionPresets?.isAutomaticSessionTrackingAllowed == true && + (userSessionPresets?.isSessionExpired == true || + userSessionPresets?.automaticSessionTrackingStatus == false + ) { + startNewSession() + userSessionPresets?.automaticSessionTrackingStatus = true + userSessionPresets?.manualSessionTrackingStatus = false + userSessionPresets?.sessionStoppedStatus = false } else { - userDefaults?.write(.automaticSessionTrackingStatus, value: false) + userSessionPresets?.automaticSessionTrackingStatus = false } } func process(message: T?) -> T? where T: Message { guard var workingMessage = message else { return message } - if isSessionTrackingAllowed { - if let sessionId = self.sessionId { + if userSessionPresets?.isSessionTrackingAllowed == true { + if let sessionId = userSessionPresets?.sessionId { workingMessage.sessionId = sessionId - if isNewSessionStarted { + if userSessionPresets?.isNewSessionStarted == true { workingMessage.sessionStart = true - isNewSessionStarted = false + userSessionPresets?.isNewSessionStarted = false } } - let currentEventTimeStamp = Utility.getTimeStamp() - userDefaults?.write(.lastEventTimeStamp, value: currentEventTimeStamp) + userSessionPresets?.lastEventTimeStamp = Utility.getTimeStamp() } return workingMessage } - - // This method should be called only when session tracking is allowed - private func isSessionExpired() -> Bool { - guard let lastEventTimeStamp = self.lastEventTimeStamp, let sessionTimeOut = self.sessionTimeOut else { - return true - } - - let timeDifference: TimeInterval = TimeInterval(abs(Utility.getTimeStamp() - lastEventTimeStamp)) - return timeDifference >= Double(sessionTimeOut / 1000) - } - - func startNewSession(_ sessionId: Int? = nil) { - isNewSessionStarted = true - userDefaults?.write(.sessionId, value: sessionId ?? Utility.getTimeStamp()) - client?.logDebug(LogMessages.newSession.description) - } } extension UserSessionPlugin { - func startManualSession(_ sessionId: Int? = nil) { - userDefaults?.write(.automaticSessionTrackingStatus, value: false) - userDefaults?.write(.manualSessionTrackingStatus, value: true) - userDefaults?.write(.sessionStoppedStatus, value: false) - userDefaults?.remove(.lastEventTimeStamp) + private func startNewSession(_ sessionId: Int? = nil) { + userSessionPresets?.isNewSessionStarted = true + userSessionPresets?.sessionId = sessionId ?? Utility.getTimeStamp() + client?.logger.logDebug(.newSession) + } + + func startSession(_ sessionId: Int? = nil) { + userSessionPresets?.automaticSessionTrackingStatus = false + userSessionPresets?.manualSessionTrackingStatus = true + userSessionPresets?.sessionStoppedStatus = false + userSessionPresets?.lastEventTimeStamp = nil startNewSession(sessionId) } func endSession() { - userDefaults?.write(.automaticSessionTrackingStatus, value: false) - userDefaults?.write(.manualSessionTrackingStatus, value: false) - userDefaults?.write(.sessionStoppedStatus, value: true) - userDefaults?.remove(.lastEventTimeStamp) + userSessionPresets?.sessionId = nil + userSessionPresets?.automaticSessionTrackingStatus = false + userSessionPresets?.manualSessionTrackingStatus = false + userSessionPresets?.sessionStoppedStatus = true + userSessionPresets?.lastEventTimeStamp = nil } func reset() { - if isSessionTrackingAllowed { - if automaticSessionTrackingStatus { - userDefaults?.remove(.lastEventTimeStamp) + if userSessionPresets?.isSessionTrackingAllowed == true { + if userSessionPresets?.automaticSessionTrackingStatus == true { + userSessionPresets?.lastEventTimeStamp = nil } startNewSession() } } func refreshSessionIfNeeded() { - if isSessionTrackingAllowed, automaticSessionTrackingStatus, isSessionExpired() { + if let userSessionPresets = userSessionPresets, + userSessionPresets.isSessionTrackingAllowed, + userSessionPresets.automaticSessionTrackingStatus, + userSessionPresets.isSessionExpired { startNewSession() } } } -extension Controller { +extension RSClientCore { internal func refreshSessionIfNeeded() { if let userSessionPlugin = getPlugin(type: UserSessionPlugin.self) { userSessionPlugin.refreshSessionIfNeeded() @@ -150,21 +194,21 @@ extension Controller { extension RSClient { public func startSession() { if let userSessionPlugin = getPlugin(type: UserSessionPlugin.self) { - userSessionPlugin.startManualSession() + userSessionPlugin.startSession() } else { - logDebug(LogMessages.sessionCanNotStart.description) + logger.logDebug(.sessionCanNotStart) } } public func startSession(_ sessionId: Int) { guard String(sessionId).count >= 10 else { - logError(LogMessages.sessionIdLengthInvalid(sessionId).description) + logger.logError(.sessionIdLengthInvalid(sessionId)) return } if let userSessionPlugin = getPlugin(type: UserSessionPlugin.self) { - userSessionPlugin.startManualSession(sessionId) + userSessionPlugin.startSession(sessionId) } else { - logDebug(LogMessages.sessionCanNotStart.description) + logger.logDebug(.sessionCanNotStart) } } diff --git a/Sources/Classes/Core/Policies/FlushPolicy.swift b/Sources/Classes/Core/Policies/FlushPolicy.swift index e3648b22..27f6234a 100644 --- a/Sources/Classes/Core/Policies/FlushPolicy.swift +++ b/Sources/Classes/Core/Policies/FlushPolicy.swift @@ -15,12 +15,12 @@ public protocol FlushPolicy { } class CountBasedFlushPolicy: FlushPolicy { - let config: Config + let config: Configuration @ReadWriteLock var count = 0 - init(config: Config) { + init(config: Configuration) { self.config = config } diff --git a/Sources/Classes/Core/RSClient.swift b/Sources/Classes/Core/RSClient.swift index fa230350..17e44082 100644 --- a/Sources/Classes/Core/RSClient.swift +++ b/Sources/Classes/Core/RSClient.swift @@ -8,373 +8,386 @@ import Foundation -public class RSClient { - let config: Config - let controller: Controller +/// An entry point to RudderStack SDK. +/// +/// Initialize the default instance of RudderStack SDK. +/// +/// ```swift +/// if let configuration: Configuration = Configuration( +/// writeKey: "", +/// dataPlaneURL: "" +/// ) { +/// RSClient.initialize( +/// with: configuration +/// ) +/// } +/// ``` +/// +public class RSClient: RSClientProtocol { + + /// Configuration of RudderStack SDK. + public var configuration: Configuration { + core.configuration + } + + /// A UserDefaultsWorker instance. + public var userDefaultsWorker: UserDefaultsWorkerProtocol { + core.userDefaultsWorker + } + + /// StorageWorker instance. + public var storageWorker: StorageWorkerProtocol { + core.storageWorker + } + + /// SessionStorage instance. + public var sessionStorage: SessionStorageProtocol { + core.sessionStorage + } + + /// Log information. + public var logger: Logger { + core.logger + } + + /// Given instance name. + public var instanceName: String { + core.instanceName + } + + private let core: RSClientCore - /** - Initialize this instance of RSClient with a given configuration setup. - - Parameters: - - config: The configuration to use - # Example # - ``` - let config: Config = Config(writeKey: WRITE_KEY) - .dataPlaneURL(DATA_PLANE_URL) - - RSClient.sharedInstance().configure(with: config) - ``` - */ - - public init( - config: Config, + /// Creates a `RSClient` instance. + /// - Parameters: + /// - configuration: The SDK configuration. + /// - instanceName: The core instance name. This value will be used for data persistency and differentiate between instances. + /// - database: The developer-choice `SQLite` database. Can be used `SQLCipher` as well. + /// - storage: The developer-choice storage. Can be used file system and any other storage implementation. + /// - userDefaults: The developer-choice `UserDefaults` implementation. + /// - apiClient: The developer-choice networking client. Can be used `Alamofire`, `Moya`, etc.... + /// - sourceConfigDownloader: The developer-choice source config download implementation. + /// - dataUploader: The developer-choice source upload data(events) to server implementation. + /// - storageMigrator: The developer-choice storage migration implementation, if any. + /// - logger: The developer-choice logger. + required init( + configuration: Configuration, + instanceName: String, database: Database? = nil, storage: Storage? = nil, userDefaults: UserDefaults? = nil, apiClient: APIClient? = nil, sourceConfigDownloader: SourceConfigDownloaderType? = nil, dataUploader: DataUploaderType? = nil, - logger: LoggerProtocol? = nil + storageMigrator: StorageMigrator? = nil ) { - self.config = config - self.controller = Controller( - config: config, + core = RSClientCore( + configuration: configuration, + instanceName: instanceName, database: database, storage: storage, userDefaults: userDefaults, sourceConfigDownloader: sourceConfigDownloader, dataUploader: dataUploader, - apiClient: apiClient, - logger: logger + apiClient: apiClient ) addPlugins() - } -} - -extension RSClient { - public func addPlugin(_ plugin: Plugin) { - plugin.client = self - controller.addPlugin(plugin) - } - - public func removePlugin(_ plugin: Plugin) { - controller.removePlugin(plugin) - } - - public func getPluginList(by pluginType: PluginType) -> [Plugin]? { - return controller.getPluginList(by: pluginType) - } - - public func getPlugin(type: T.Type) -> T? { - return controller.getPlugin(type: type) - } - - public func associatePlugins(_ handler: (Plugin) -> Void) { - controller.associatePlugins(handler) + ClientRegistry.register(self, name: instanceName) + } + + /// Initialize the RudderStack SDK. + /// + /// Initialize the default instance of RudderStack SDK. + /// + /// ```swift + /// if let configuration: Configuration = Configuration( + /// writeKey: "", + /// dataPlaneURL: "" + /// ) { + /// RSClient.initialize( + /// with: configuration + /// ) + /// } + /// ``` + /// + /// - Parameters: + /// - configuration: The SDK configuration. + /// - instanceName: The core instance name. This value will be used for data persistency and differentiate between instances. + /// - database: The developer-choice `SQLite` database implementation, if any. + /// - storage: The developer-choice storage implementation, if any. + /// - userDefaults: The developer-choice `UserDefaults` implementation, if any. + /// - apiClient: The developer-choice networking client implementation, if any. + /// - sourceConfigDownloader: The developer-choice source config download implementation, if any. + /// - dataUploader: The developer-choice source upload data(events) to server implementation, if any. + /// - logger: The developer-choice logger, if any. + /// - storageMigrator: The developer-choice storage migration implementation, if any. + /// - Returns: An instance of `RSClient`. + /// + @discardableResult + public static func initialize( + with configuration: Configuration, + instanceName: String = ClientRegistry.defaultInstanceName, + database: Database? = nil, + storage: Storage? = nil, + userDefaults: UserDefaults? = nil, + apiClient: APIClient? = nil, + sourceConfigDownloader: SourceConfigDownloaderType? = nil, + dataUploader: DataUploaderType? = nil + ) -> RSClient { + if instanceName.correctified == ClientRegistry.defaultInstanceName, ClientRegistry.isRegistered(instanceName: instanceName.correctified), + let instance = ClientRegistry.default { + return instance + } + let instance = self.init( + configuration: configuration, + instanceName: instanceName.correctified, + database: database, + storage: storage, + userDefaults: userDefaults, + apiClient: apiClient, + sourceConfigDownloader: sourceConfigDownloader, + dataUploader: dataUploader + ) + return instance } } extension RSClient { - /** - API for track your event - - Parameters: - - eventName: Name of the event you want to track - - properties: Properties you want to pass with the track call - - option: MessageOptions related to this track call - # Example # - ``` - RSClient.sharedInstance().track("simple_track_with_props", properties: ["key_1": "value_1", "key_2": "value_2"], option: MessageOption()) - ``` - */ - - public func track(_ eventName: String, properties: TrackProperties, option: MessageOption) { - controller.track(eventName, properties: properties, option: option) - } - - public func track(_ eventName: String, properties: TrackProperties) { - controller.track(eventName, properties: properties, option: nil) - } - public func track(_ eventName: String, option: MessageOption) { - controller.track(eventName, properties: nil, option: option) + /// Returns the RudderStack instance for the given name. + /// + /// - Parameter name: The name of the instance to get. + /// - Returns: The instance by the name if exists, otherwise nil. + public static func instance(named name: String) -> RSClient? { + ClientRegistry.instance(named: name) } - public func track(_ eventName: String) { - controller.track(eventName, properties: nil, option: nil) + /// Returns the default instance if registered. + public static var `default`: RSClient? { + ClientRegistry.default } - /** - API for add the user to a group - - Parameters: - - userId: User id of your user - - traits: Other user properties - - option: IdentifyOptions related to this identify call - # Example # - ``` - RSClient.sharedInstance().identify("user_id", traits: ["email": "abc@def.com"], option: IdentifyOption()) - ``` - */ - - public func identify(_ userId: String, traits: IdentifyTraits, option: IdentifyOptionType) { - controller.identify(userId, traits: traits, option: option) + /// Check if an instance with specific name is registered. + /// - Parameter instanceName: The name of the instance to check. + /// - Returns: `true` if an instance with the given name is registered, otherwise `false`. + public static func isRegistered(instanceName: String) -> Bool { + return ClientRegistry.isRegistered(instanceName: instanceName) } - public func identify(_ userId: String, traits: IdentifyTraits) { - controller.identify(userId, traits: traits, option: nil) + /// Unregister a instance for the given name. + /// - Parameter name: The name of the instance to unregister. + public static func unregisterInstance(named name: String) { + ClientRegistry.unregisterInstance(named: name) } +} - public func identify(_ userId: String, option: IdentifyOptionType) { - controller.identify(userId, traits: nil, option: option) - } +extension RSClient { - public func identify(_ userId: String) { - controller.identify(userId, traits: nil, option: nil) + /// Add a Plugin instance. + /// + /// - Parameter plugin: The Plugin instance. + public func addPlugin(_ plugin: Plugin) { + plugin.client = self + core.addPlugin(plugin) } - /** - API for record screen - - Parameters: - - screenName: Name of the screen - - properties: Properties you want to pass with the screen call - - option: MessageOptions related to this screen call - # Example # - ``` - RSClient.sharedInstance().screen("ViewController", properties: ["key_1": "value_1", "key_2": "value_2"], option: MessageOption()) - ``` - */ - - public func screen(_ screenName: String, properties: ScreenProperties, option: MessageOption) { - controller.screen(screenName, category: nil, properties: properties, option: option) + /// Remove a Plugin instance. + /// - Parameter plugin: The Plugin instance. + public func removePlugin(_ plugin: Plugin) { + core.removePlugin(plugin) } - public func screen(_ screenName: String, properties: ScreenProperties) { - controller.screen(screenName, category: nil, properties: properties, option: nil) + /// Retrieve all Plugin instance list. + /// + /// - Returns: The list of all Plugins. + public func getAllPlugins() -> [Plugin]? { + return core.getAllPlugins() } - public func screen(_ screenName: String, option: MessageOption) { - controller.screen(screenName, category: nil, properties: nil, option: option) + /// Retrieve all destination Plugin instance list. + /// + /// - Returns: The list of Plugins if any. + public func getDestinationPlugins() -> [DestinationPlugin]? { + return core.getDestinationPlugins() } - public func screen(_ screenName: String, category: String, properties: ScreenProperties, option: MessageOption) { - controller.screen(screenName, category: category, properties: properties, option: option) + /// Retrieve all default Plugin instance list. + /// + /// - Returns: The list of default Plugins if any. + public func getDefaultPlugins() -> [Plugin]? { + return core.getDefaultPlugins() } - - public func screen(_ screenName: String, category: String, properties: ScreenProperties) { - controller.screen(screenName, category: category, properties: properties, option: nil) + + /// Retrive a Plugin instance by instance type. + /// - Parameter type: The Plugin instance type. + /// - Returns: The instance of Plugin if any. + public func getPlugin(type: T.Type) -> T? { + return core.getPlugin(type: type) } - public func screen(_ screenName: String, category: String, option: MessageOption) { - controller.screen(screenName, category: category, properties: nil, option: option) - } - - public func screen(_ screenName: String, category: String) { - controller.screen(screenName, category: category, properties: nil, option: nil) + /// Associate a handler to all the Plugin list. + /// + /// - Parameter handler: The closure which takes a Plugin as a parameter. + public func associatePlugins(_ handler: (Plugin) -> Void) { + core.associatePlugins(handler) } +} - public func screen(_ screenName: String) { - controller.screen(screenName, category: nil, properties: nil, option: nil) - } - - /** - API for add the user to a group - - Parameters: - - groupId: Group ID you want your user to attach to - - traits: Traits of the group - - option: MessageOptions related to this group call - # Example # - ``` - RSClient.sharedInstance().group("sample_group_id", traits: ["key_1": "value_1", "key_2": "value_2"], option: MessageOption()) - ``` - */ - - public func group(_ groupId: String, traits: GroupTraits, option: MessageOption) { - controller.group(groupId, traits: traits, option: option) - } - - public func group(_ groupId: String, traits: GroupTraits) { - controller.group(groupId, traits: traits, option: nil) - } - - public func group(_ groupId: String, option: MessageOption) { - controller.group(groupId, traits: nil, option: option) - } - - public func group(_ groupId: String) { - controller.group(groupId, traits: nil, option: nil) - } - - /** - API for add the user to a group - - Parameters: - - newId: New userId for the user - - option: MessageOptions related to this alias call - # Example # - ``` - RSClient.sharedInstance().alias("user_id", option: MessageOption()) - ``` - */ - - public func alias(_ newId: String, option: MessageOption) { - controller.alias(newId, option: option) - } +extension RSClient { - public func alias(_ newId: String) { - controller.alias(newId, option: nil) + /// Record user's activity. + /// + /// - Parameters: + /// - eventName: The name of the activity. + /// - properties: Extra data properties regarding the event, if any. + /// - option: Extra event options, if any. + public func track(_ eventName: String, properties: TrackProperties? = nil, option: MessageOption? = nil) { + core.track(eventName, properties: properties, option: option) + } + + /// Set current user's information + /// + /// - Parameters: + /// - userId: User's ID. + /// - traits: User's additional information, if any. + /// - option: Event level option, if any. + public func identify(_ userId: String, traits: IdentifyTraits? = nil, option: IdentifyOptionType? = nil) { + core.identify(userId, traits: traits, option: option) + } + + /// Track a screen with name, category. + /// + /// - Parameters: + /// - screenName: The name of the screen viewed by an user. + /// - category: The category or type of screen, if any. + /// - properties: Extra data properties regarding the screen call, if any. + /// - option: Extra screen event options, if any. + public func screen(_ screenName: String, category: String? = nil, properties: ScreenProperties? = nil, option: MessageOption? = nil) { + core.screen(screenName, category: category, properties: properties, option: option) + } + + /// Associate an user to a company or organization. + /// + /// - Parameters: + /// - groupId: The company's ID. + /// - traits: Extra information of the company, if any. + /// - option: Event level options, if any. + public func group(_ groupId: String, traits: GroupTraits? = nil, option: MessageOption? = nil) { + core.group(groupId, traits: traits, option: option) + } + + /// Associate the current user to a new identification. + /// + /// - Parameters: + /// - groupId: User's new ID. + /// - option: Event level options, if any. + public func alias(_ newId: String, option: MessageOption? = nil) { + core.alias(newId, option: option) } } extension RSClient { - /** - Returns the anonymousId currently in use. - */ + + /// Returns the anonymousId currently in use. public var anonymousId: String? { - controller.anonymousId + core.anonymousId } - /** - Returns the userId that was specified in the last identify call. - */ + /// Returns the userId that was specified in the last identify call. public var userId: String? { - controller.userId + core.userId } - /** - Returns the context that were specified in the last call. - */ + /// Returns the context that were specified in the last call. public var context: Context? { - controller.context + core.context } - /** - Returns the traits that were specified in the last identify call. - */ + /// Returns the traits that were specified in the last identify call. public var traits: IdentifyTraits? { - controller.traits + core.traits } - /** - Returns the version ("BREAKING.FEATURE.FIX" format) of this library in use. - */ + /// Returns the version ("BREAKING.FEATURE.FIX" format) of this library in use. public var version: String { return RSVersion } - /** - Returns the config set by developer while initialisation. - */ - public var configuration: Config? { - controller.config - } - - /** - Returns id of an active session. - */ - public var sessionId: String? { - controller.sessionId + /// Returns id of an active session. + public var sessionId: Int? { + core.sessionId } } extension RSClient { - /** - API for flush any queued events. This command will also be sent to each destination present in the system. - */ + + /// API for flush any queued events. This command will also be sent to each destination present in the system. public func flush() { - controller.flush() + core.flush() } - /** - API for reset current slate. Traits, UserID's, anonymousId, etc are all cleared or reset. This command will also be sent to each destination present in the system. - */ + /// Reset current slate. Traits, UserID's, anonymousId, etc are all cleared or reset. + /// This command will also be sent to each destination present in the system. + /// + /// - Parameter refreshAnonymousId: Refresh anonymous ID as well. public func reset(and refreshAnonymousId: Bool) { - controller.reset(and: refreshAnonymousId) + core.reset(and: refreshAnonymousId) } } extension RSClient { - /** - API for setting unique identifier of every call. - - Parameters: - - anonymousId: Unique identifier of every event - # Example # - ``` - RSClient.sharedInstance().setAnonymousId("sample_anonymous_id") - ``` - */ + /// API for setting unique identifier of every call. + /// + /// - Parameters: + /// - anonymousId: Unique identifier of every event public func setAnonymousId(_ anonymousId: String) { - controller.setAnonymousId(anonymousId) + core.setAnonymousId(anonymousId) } - /** - API for setting enable/disable sending the events across all the event calls made using the SDK to the specified destinations. - - Parameters: - - option: Options related to every API call - # Example # - ``` - let defaultOption = Option() - defaultOption.putIntegration("Amplitude", isEnabled: true) - - RSClient.sharedInstance().setOption(defaultOption) - ``` - */ + /// API for setting enable/disable sending the events across all the event calls made using the SDK to the specified destinations. + /// + /// - Parameters: + /// - option: Options related to every API call public func setOption(_ option: Option) { - controller.setOption(option) + core.setOption(option) } - /** - API for setting token under context.device.token. - - Parameters: - - token: Token of the device - # Example # - ``` - RSClient.sharedInstance().setDeviceToken("sample_device_token") - ``` - */ + /// API for setting device token for Push Notifications to the destinations. + /// + /// - Parameters: + /// - token: Token of the device public func setDeviceToken(_ token: String) { - controller.setDeviceToken(token) + core.setDeviceToken(token) } - /** - API for setting identifier under context.device.advertisingId. - - Parameters: - - advertisingId: IDFA value - # Example # - ``` - RSClient.sharedInstance().setAdvertisingId("sample_advertising_id") - ``` - */ + /// API for setting identifier under context.device.advertisingId. + /// - Parameters: + /// - advertisingId: IDFA value public func setAdvertisingId(_ advertisingId: String) { - controller.setAdvertisingId(advertisingId) + core.setAdvertisingId(advertisingId) } - /** - API for app tracking consent management. - - Parameters: - - appTrackingConsent: App tracking consent - # Example # - ``` - RSClient.sharedInstance().setAppTrackingConsent(.authorize) - ``` - */ + /// API for the Data Tracking Consent given by the user of the app. + /// + /// - Parameters: + /// - appTrackingConsent: The Data Tracking Consent given by the user of the app public func setAppTrackingConsent(_ appTrackingConsent: AppTrackingConsent) { - controller.setAppTrackingConsent(appTrackingConsent) - } - - /** - API for enable or disable tracking user activities. - - Parameters: - - status: Enable or disable tracking - # Example # - ``` - RSClient.sharedInstance().setOptOutStatus(false) - ``` - */ + core.setAppTrackingConsent(appTrackingConsent) + } + + /// API for enable or disable tracking user activities. + /// + /// - Parameters: + /// - status: Enable or disable tracking. public func setOptOutStatus(_ status: Bool) { - controller.setOptOutStatus(status) + core.setOptOutStatus(status) } } extension RSClient { + + /// Add the default Plugins. private func addPlugins() { - addPlugin(ReplayQueuePlugin(queue: DispatchQueue(label: "replayQueuePlugin".queueLabel()))) + addPlugin(ReplayQueuePlugin(queue: DispatchQueue(label: "replayQueuePlugin".queueLabel(instanceName)))) addPlugin(IntegrationPlugin()) addPlugin(UserSessionPlugin()) addPlugin(ContextPlugin()) diff --git a/Sources/Classes/Core/Controller.swift b/Sources/Classes/Core/RSClientCore.swift similarity index 63% rename from Sources/Classes/Core/Controller.swift rename to Sources/Classes/Core/RSClientCore.swift index 5b1f7a7a..2149a81f 100644 --- a/Sources/Classes/Core/Controller.swift +++ b/Sources/Classes/Core/RSClientCore.swift @@ -8,132 +8,155 @@ import Foundation -class Controller { - let config: Config - let database: Database +class RSClientCore { + let configuration: Configuration let storage: Storage - let storageWorker: StorageWorker - let userDefaults: UserDefaultsWorkerType + let storageWorker: StorageWorkerProtocol + let userDefaultsWorker: UserDefaultsWorkerProtocol let serviceManager: ServiceType let sourceConfigDownloader: SourceConfigDownloaderType let logger: Logger + let instanceName: String let downloadUploadBlockers: DownloadUploadBlockers = DownloadUploadBlockers() - let sessionStorage: SessionStorage = SessionStorage() + let sessionStorage: SessionStorageProtocol = SessionStorage() + var database: Database? var applicationSate: ApplicationState? var screenRecording: ScreenRecording? var dataUpload: DataUpload? var dataUploader: DataUploaderType? var sourceConfigDownload: SourceConfigDownload? + var storageMigration: StorageMigration? + var storageMigrator: StorageMigrator? var flushPolicies: [FlushPolicy] - var pluginList: [PluginType: [Plugin]] = [ + @ReadWriteLock var pluginList: [PluginType: [Plugin]] = [ .default: [Plugin](), .destination: [Plugin]() ] var userInfo: UserInfo { - let userId: String? = userDefaults.read(.userId) - let traits: JSON? = userDefaults.read(.traits) - var anonymousId: String? = userDefaults.read(.anonymousId) + let userId: String? = userDefaultsWorker.read(.userId) + let traits: JSON? = userDefaultsWorker.read(.traits) + var anonymousId: String? = userDefaultsWorker.read(.anonymousId) if anonymousId == nil { anonymousId = Utility.getUniqueId() - userDefaults.write(.anonymousId, value: anonymousId) + userDefaultsWorker.write(.anonymousId, value: anonymousId) } return UserInfo(anonymousId: anonymousId, userId: userId, traits: traits) } @ReadWriteLock var isEnable: Bool = true init( - config: Config, - database: Database? = nil, + configuration: Configuration, + instanceName: String, + database: Database? = nil, storage: Storage? = nil, userDefaults: UserDefaults? = nil, sourceConfigDownloader: SourceConfigDownloaderType? = nil, dataUploader: DataUploaderType? = nil, apiClient: APIClient? = nil, - logger: LoggerProtocol? = nil, - applicationState: ApplicationState? = nil + applicationState: ApplicationState? = nil, + storageMigrator: StorageMigrator? = nil ) { - self.config = config - self.logger = Logger(logger: logger ?? ConsoleLogger(logLevel: config.logLevel)) - self.database = database ?? DefaultDatabase(path: Device.current.directoryPath, name: "rl_persistence.sqlite") - self.storage = storage ?? DefaultStorage( - database: self.database, - logger: self.logger + self.configuration = configuration + self.instanceName = instanceName + self.logger = Logger( + logger: configuration.logger ?? ConsoleLogger( + logLevel: configuration.logLevel, + instanceName: instanceName + ) ) - self.storageWorker = DefaultStorageWorker( + if let storage = storage { + self.storage = storage + } else { + let defaultDatabase = SQLiteDatabase( + path: Device.current.directoryPath, + name: "rl_persistence_\(instanceName).sqlite" + ) + self.database = defaultDatabase + let defaultStorage = SQLiteStorage( + database: defaultDatabase, + logger: self.logger + ) + self.storage = defaultStorage + } + + self.storageWorker = StorageWorker( storage: self.storage, - queue: DispatchQueue(label: "defaultStorageWorker".queueLabel()) + queue: DispatchQueue(label: "defaultStorageWorker".queueLabel(instanceName)) ) self.storageWorker.open() - let userDefaultsQueue = DispatchQueue(label: "defaultUserDefaults".queueLabel()) + let userDefaultsQueue = DispatchQueue(label: "defaultUserDefaults".queueLabel(instanceName)) if let userDefaults = userDefaults { - self.userDefaults = UserDefaultsWorker(userDefaults: userDefaults, queue: userDefaultsQueue) + self.userDefaultsWorker = UserDefaultsWorker(userDefaults: userDefaults, queue: userDefaultsQueue) } else { - self.userDefaults = UserDefaultsWorker(suiteName: "defaultUserDefaults".userDefaultsSuitName(), queue: userDefaultsQueue) + self.userDefaultsWorker = UserDefaultsWorker(suiteName: "defaultUserDefaults".userDefaultsSuitName(instanceName), queue: userDefaultsQueue) } self.serviceManager = ServiceManager( apiClient: apiClient ?? URLSessionClient( session: URLSession.defaultSession() ), - writeKey: config.writeKey + writeKey: configuration.writeKey ) self.sourceConfigDownloader = sourceConfigDownloader ?? SourceConfigDownloader( serviceManager: self.serviceManager, - controlPlaneUrl: config.controlPlaneURL + controlPlaneUrl: configuration.controlPlaneURL ) self.dataUploader = dataUploader self.flushPolicies = [ - CountBasedFlushPolicy(config: config) + CountBasedFlushPolicy(config: configuration) ] - if !config.flushPolicies.isEmpty { - self.flushPolicies.append(contentsOf: config.flushPolicies) + if !configuration.flushPolicies.isEmpty { + self.flushPolicies.append(contentsOf: configuration.flushPolicies) } + self.storageMigrator = storageMigrator trackApplicationState() recordScreenViews() fetchSourceConfig() + logConfigValidationErrors() } + } -extension Controller { - func trackApplicationState() { +extension RSClientCore { + private func trackApplicationState() { applicationSate = ApplicationState.current( notificationCenter: NotificationCenter.default, - userDefaults: self.userDefaults + userDefaults: self.userDefaultsWorker ) applicationSate?.observeNotifications() applicationSate?.trackApplicationStateMessage = { [weak self] applicationStateMessage in - guard let self = self, self.config.trackLifecycleEvents else { return } + guard let self = self, self.configuration.trackLifecycleEvents else { return } self.track(applicationStateMessage.state.eventName, properties: applicationStateMessage.properties) } applicationSate?.refreshSessionIfNeeded = { [weak self] in - guard let self = self, self.config.trackLifecycleEvents else { return } + guard let self = self, self.configuration.trackLifecycleEvents else { return } self.refreshSessionIfNeeded() } } - func recordScreenViews() { + private func recordScreenViews() { screenRecording = ScreenRecording() screenRecording?.capture = { [weak self] screenViewsMessage in - guard let self = self, self.config.recordScreenViews else { return } + guard let self = self, self.configuration.recordScreenViews else { return } self.screen(screenViewsMessage.screenName, properties: screenViewsMessage.properties) } } - func fetchSourceConfig() { + private func fetchSourceConfig() { let sourceConfigDownloadRetryFactors = RetryFactors( retryPreset: DownloadUploadRetryPreset.defaultDownload(), current: TimeInterval(1) ) - let sourceConfigDownloadRetryPolicy = config.sourceConfigDownloadRetryPolicy ?? ExponentialRetryPolicy( + let sourceConfigDownloadRetryPolicy = configuration.sourceConfigDownloadRetryPolicy ?? ExponentialRetryPolicy( retryFactors: sourceConfigDownloadRetryFactors ) let downloader = SourceConfigDownloadWorker( sourceConfigDownloader: sourceConfigDownloader, downloadBlockers: downloadUploadBlockers, - userDefaults: userDefaults, + userDefaults: userDefaultsWorker, queue: DispatchQueue( - label: "sourceConfigDownload".queueLabel(), + label: "sourceConfigDownload".queueLabel(instanceName), autoreleaseFrequency: .workItem, target: .global(qos: .utility) ), @@ -145,42 +168,45 @@ extension Controller { sourceConfigDownload = SourceConfigDownload(downloader: downloader) - sourceConfigDownload?.sourceConfig = { [weak self] sourceConfig in + sourceConfigDownload?.sourceConfig = { [weak self] sourceConfig, needsDatabaseMigration in guard let self = self else { return } + if needsDatabaseMigration { + self.migrateStorage() + } self.isEnable = sourceConfig.enabled self.updateSourceConfig(sourceConfig) - self.userDefaults.write(.sourceConfig, value: sourceConfig) + self.userDefaultsWorker.write(.sourceConfig, value: sourceConfig) if self.dataUpload == nil { let dataUploadRetryFactors = RetryFactors( retryPreset: DownloadUploadRetryPreset.defaultUpload( - minTimeout: TimeInterval(self.config.sleepTimeOut) + minTimeout: TimeInterval(self.configuration.sleepTimeOut) ), - current: TimeInterval(self.config.sleepTimeOut) + current: TimeInterval(self.configuration.sleepTimeOut) ) - let dataUploadRetryPolicy = self.config.dataUploadRetryPolicy ?? ExponentialRetryPolicy( + let dataUploadRetryPolicy = self.configuration.dataUploadRetryPolicy ?? ExponentialRetryPolicy( retryFactors: dataUploadRetryFactors ) let dataResidency = DataResidency( - dataResidencyServer: self.config.dataResidencyServer, + dataResidencyServer: self.configuration.dataResidencyServer, sourceConfig: sourceConfig ) let dataUploader = self.dataUploader ?? DataUploader( serviceManager: self.serviceManager, - anonymousId: self.userDefaults.read(.anonymousId) ?? "", - gzipEnabled: self.config.gzipEnabled, - dataPlaneUrl: dataResidency.dataPlaneUrl ?? self.config.dataPlaneURL + anonymousId: self.userDefaultsWorker.read(.anonymousId) ?? "", + gzipEnabled: self.configuration.gzipEnabled, + dataPlaneUrl: dataResidency.dataPlaneUrl ?? self.configuration.dataPlaneURL ) let uploader = DataUploadWorker( dataUploader: dataUploader, dataUploadBlockers: self.downloadUploadBlockers, storageWorker: self.storageWorker, - config: self.config, + config: self.configuration, queue: DispatchQueue( - label: "dataUploadWorker".queueLabel(), + label: "dataUploadWorker".queueLabel(self.instanceName), autoreleaseFrequency: .workItem, target: .global(qos: .utility) ), @@ -194,15 +220,44 @@ extension Controller { if !sourceConfig.enabled { self.dataUpload?.cancel() } else { - let dataResidency = DataResidency(dataResidencyServer: self.config.dataResidencyServer, sourceConfig: sourceConfig) - self.dataUploader?.updateDataPlaneUrl(dataResidency.dataPlaneUrl ?? self.config.dataPlaneURL) + let dataResidency = DataResidency(dataResidencyServer: self.configuration.dataResidencyServer, sourceConfig: sourceConfig) + self.dataUploader?.updateDataPlaneUrl(dataResidency.dataPlaneUrl ?? self.configuration.dataPlaneURL) + } + } + } + } + + private func logConfigValidationErrors() { + configuration.configValidationErrorList.forEach({ logger.logWarning(LogMessages.customMessage($0.description)) }) + } + + private func migrateStorage() { + if let storageMigrator = storageMigrator { + storageMigration = StorageMigration(storageMigrator: storageMigrator) + } else { + let oldDatabase = SQLiteDatabase(path: Device.current.directoryPath, name: "rl_persistence.sqlite") + let oldSQLiteStorage = SQLiteStorage(database: oldDatabase, logger: logger) + let storageMigrator = StorageMigratorV1V2(oldSQLiteStorage: oldSQLiteStorage, currentStorage: storage) + storageMigration = StorageMigration(storageMigrator: storageMigrator) + } + do { + try storageMigration?.migrate() + logger.logDebug(.storageMigrationSuccess) + } catch { + if let error = error as? StorageError { + if error == .databaseNotExists { + logger.logDebug(.customMessage(error.description)) + } else { + logger.logError(.storageMigrationFailed(error)) } + } else { + logger.logDebug(.customMessage(error.localizedDescription)) } } } } -extension Controller { +extension RSClientCore { func updateSourceConfig(_ sourceConfig: SourceConfig) { pluginList.forEach { (_, value) in value.forEach { plugin in @@ -210,7 +265,7 @@ extension Controller { } } } - + #warning("add sync queue") func addPlugin(_ plugin: Plugin) { if var list = pluginList[plugin.type] { list.addPlugin(plugin) @@ -227,7 +282,21 @@ extension Controller { } } - func getPluginList(by pluginType: PluginType) -> [Plugin]? { + func getAllPlugins() -> [Plugin]? { + return pluginList.flatMap { (_, value) in + return value + } + } + + func getDestinationPlugins() -> [DestinationPlugin]? { + return getPluginList(by: .destination) as? [DestinationPlugin] + } + + func getDefaultPlugins() -> [Plugin]? { + return getPluginList(by: .default) + } + + private func getPluginList(by pluginType: PluginType) -> [Plugin]? { return pluginList[pluginType] } @@ -255,9 +324,9 @@ extension Controller { } } -extension Controller { +extension RSClientCore { func track(_ eventName: String, properties: TrackProperties? = nil, option: MessageOption? = nil) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOutAndEventDrop) return } @@ -270,7 +339,7 @@ extension Controller { } func screen(_ screenName: String, category: String? = nil, properties: ScreenProperties? = nil, option: MessageOption? = nil) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOutAndEventDrop) return } @@ -288,7 +357,7 @@ extension Controller { } func group(_ groupId: String, traits: [String: String]? = nil, option: MessageOption? = nil) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOutAndEventDrop) return } @@ -301,7 +370,7 @@ extension Controller { } func alias(_ newId: String, option: MessageOption? = nil) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOutAndEventDrop) return } @@ -309,19 +378,19 @@ extension Controller { logger.logWarning(.newIdNotEmpty) return } - let previousId: String? = userDefaults.read(.userId) - userDefaults.write(.userId, value: newId) + let previousId: String? = userDefaultsWorker.read(.userId) + userDefaultsWorker.write(.userId, value: newId) var dict: [String: Any] = ["id": newId] - if let json: JSON = userDefaults.read(.traits), let traits = json.dictionaryValue { + if let json: JSON = userDefaultsWorker.read(.traits), let traits = json.dictionaryValue { dict.merge(traits) { (_, new) in new } } - userDefaults.write(.traits, value: try? JSON(dict)) + userDefaultsWorker.write(.traits, value: try? JSON(dict)) let message = AliasMessage(newId: newId, previousId: previousId, option: option) process(message: message) } func identify(_ userId: String, traits: IdentifyTraits? = nil, option: IdentifyOptionType? = nil) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOutAndEventDrop) return } @@ -329,14 +398,14 @@ extension Controller { logger.logWarning(.userIdNotEmpty) return } - userDefaults.write(.userId, value: userId) + userDefaultsWorker.write(.userId, value: userId) if let traits = traits { - userDefaults.write(.traits, value: try? JSON(traits)) + userDefaultsWorker.write(.traits, value: try? JSON(traits)) } if let externalIds = option?.externalIds { - userDefaults.write(.externalId, value: try? JSON(externalIds)) + userDefaultsWorker.write(.externalId, value: try? JSON(externalIds)) } let message = IdentifyMessage(userId: userId, traits: traits, option: option) process(message: message) @@ -388,40 +457,40 @@ extension Controller { } } -extension Controller { +extension RSClientCore { var anonymousId: String? { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return nil } - return userDefaults.read(.anonymousId) + return userDefaultsWorker.read(.anonymousId) } var userId: String? { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return nil } - return userDefaults.read(.userId) + return userDefaultsWorker.read(.userId) } var context: Context? { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return nil } if let currentContext: Context = sessionStorage.read(.context) { return currentContext } - return Context(userDefaults: userDefaults) + return Context(userDefaults: userDefaultsWorker) } var traits: IdentifyTraits? { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return nil } - let traitsJSON: JSON? = Context.traits(userDefaults: userDefaults) + let traitsJSON: JSON? = Context.traits(userDefaults: userDefaultsWorker) return traitsJSON?.dictionaryValue } @@ -429,27 +498,19 @@ extension Controller { return RSVersion } - var configuration: Config? { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { - logger.logDebug(.optOut) - return nil - } - return config - } - - var sessionId: String? { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + var sessionId: Int? { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return nil } if let userSessionPlugin = getPlugin(type: UserSessionPlugin.self), let sessionId = userSessionPlugin.sessionId { - return "\(sessionId)" + return sessionId } return nil } } -extension Controller { +extension RSClientCore { func flush() { dataUpload?.flush() associatePlugins { plugin in @@ -461,7 +522,7 @@ extension Controller { func reset(and refreshAnonymousId: Bool) { if refreshAnonymousId { - userDefaults.write(.anonymousId, value: Utility.getUniqueId()) + userDefaultsWorker.write(.anonymousId, value: Utility.getUniqueId()) } reset() } @@ -476,9 +537,9 @@ extension Controller { } } -extension Controller { +extension RSClientCore { func setAnonymousId(_ anonymousId: String) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return } @@ -486,11 +547,11 @@ extension Controller { logger.logWarning(.anonymousIdNotEmpty) return } - userDefaults.write(.anonymousId, value: anonymousId) + userDefaultsWorker.write(.anonymousId, value: anonymousId) } func setOption(_ option: Option) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return } @@ -498,7 +559,7 @@ extension Controller { } func setDeviceToken(_ token: String) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return } @@ -510,7 +571,7 @@ extension Controller { } func setAdvertisingId(_ advertisingId: String) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return } @@ -524,7 +585,7 @@ extension Controller { } func setAppTrackingConsent(_ appTrackingConsent: AppTrackingConsent) { - if let optOutStatus: Bool = userDefaults.read(.optStatus), optOutStatus { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { logger.logDebug(.optOut) return } @@ -532,17 +593,17 @@ extension Controller { } func setOptOutStatus(_ status: Bool) { - userDefaults.write(.optStatus, value: status) + userDefaultsWorker.write(.optStatus, value: status) logger.logDebug(.userOptOut(status)) } } -extension Controller { +extension RSClientCore { func resetUserDefaults() { - userDefaults.remove(.traits) - userDefaults.remove(.externalId) - userDefaults.remove(.userId) + userDefaultsWorker.remove(.traits) + userDefaultsWorker.remove(.externalId) + userDefaultsWorker.remove(.userId) } } @@ -574,16 +635,22 @@ extension [Plugin] { extension String { func queueLabel(_ name: String? = nil) -> String { - if let name = name { + if let name = name, name.isNotEmpty { return "\(self).\(name).rudder.com" } return "\(self).rudder.com" } func userDefaultsSuitName(_ name: String? = nil) -> String { - if let name = name { + if let name = name, name.isNotEmpty { return "\(self).\(name).userDefaults.rudder.com" } return "\(self).userDefaults.rudder.com" } } + +extension String { + var correctified: String { + return self.isEmpty ? ClientRegistry.defaultInstanceName : self + } +} diff --git a/Sources/Classes/Core/RSClientProtocol.swift b/Sources/Classes/Core/RSClientProtocol.swift new file mode 100644 index 00000000..b3995b40 --- /dev/null +++ b/Sources/Classes/Core/RSClientProtocol.swift @@ -0,0 +1,105 @@ +// +// RSClientProtocol.swift +// Rudder +// +// Created by Pallab Maiti on 06/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +public protocol RSClientProtocol: AnyObject { + + /// Given instance name. + var instanceName: String { get } + + /// Configuration of RudderStack SDK. + var configuration: Configuration { get } + + /// A UserDefaultsWorker instance. + var userDefaultsWorker: UserDefaultsWorkerProtocol { get } + + /// StorageWorker instance. + var storageWorker: StorageWorkerProtocol { get } + + /// SessionStorage instance. + var sessionStorage: SessionStorageProtocol { get } + + /// Print logging information. + var logger: Logger { get } + + /// Returns the RudderStack instance for the given name. + /// + /// - Parameter name: The name of the instance to get. + /// - Returns: The instance by the name if exists, otherwise nil. + func addPlugin(_ plugin: Plugin) + + /// Remove a Plugin instance. + /// - Parameter plugin: The Plugin instance. + func removePlugin(_ plugin: Plugin) + + /// Retrieve all Plugin instance list. + /// + /// - Returns: The list of all Plugins. + func getAllPlugins() -> [Plugin]? + + /// Retrieve all destination Plugin instance list. + /// + /// - Returns: The list of Plugins if any. + func getDestinationPlugins() -> [DestinationPlugin]? + + /// Retrieve all default Plugin instance list. + /// + /// - Returns: The list of default Plugins if any. + func getDefaultPlugins() -> [Plugin]? + + /// Retrive a Plugin instance by instance type. + /// - Parameter type: The Plugin instance type. + /// - Returns: The instance of Plugin if any. + func getPlugin(type: T.Type) -> T? + + /// Associate a handler to all the Plugin list. + /// + /// - Parameter handler: The closure which takes a Plugin as a parameter. + func associatePlugins(_ handler: (Plugin) -> Void) + + /// Record user's activity. + /// + /// - Parameters: + /// - eventName: The name of the activity. + /// - properties: Extra data properties regarding the event, if any. + /// - option: Extra event options, if any. + func track(_ eventName: String, properties: TrackProperties?, option: MessageOption?) + + /// Set current user's information + /// + /// - Parameters: + /// - userId: User's ID. + /// - traits: User's additional information, if any. + /// - option: Event level option, if any. + func identify(_ userId: String, traits: IdentifyTraits?, option: IdentifyOptionType?) + + /// Track a screen with name, category. + /// + /// - Parameters: + /// - screenName: The name of the screen viewed by an user. + /// - category: The category or type of screen, if any. + /// - properties: Extra data properties regarding the screen call, if any. + /// - option: Extra screen event options, if any. + func screen(_ screenName: String, category: String?, properties: ScreenProperties?, option: MessageOption?) + + /// Associate an user to a company or organization. + /// + /// - Parameters: + /// - groupId: The company's ID. + /// - traits: Extra information of the company, if any. + /// - option: Event level options, if any. + func group(_ groupId: String, traits: GroupTraits?, option: MessageOption?) + + /// Associate the current user to a new identification. + /// + /// - Parameters: + /// - groupId: User's new ID. + /// - option: Event level options, if any. + func alias(_ newId: String, option: MessageOption?) +} diff --git a/Sources/Classes/Core/SourceConfigDownload/Model/Destination.swift b/Sources/Classes/Core/SourceConfigDownload/Model/Destination.swift index f32e5d04..d57e5768 100644 --- a/Sources/Classes/Core/SourceConfigDownload/Model/Destination.swift +++ b/Sources/Classes/Core/SourceConfigDownload/Model/Destination.swift @@ -73,29 +73,21 @@ public struct Destination: Codable, Equatable { } var blackListedEvents: [String]? { - var eventList: [String]? if let events = config?.dictionaryValue?["blacklistedEvents"] as? [[String: String]] { - eventList = [String]() - for event in events { - if let eventName = event["eventName"], eventName.isNotEmpty { - eventList?.append(eventName) - } + return events.compactMap { dict in + dict["eventName"] } } - return eventList + return nil } var whiteListedEvents: [String]? { - var eventList: [String]? if let events = config?.dictionaryValue?["whitelistedEvents"] as? [[String: String]] { - eventList = [String]() - for event in events { - if let eventName = event["eventName"], eventName.isNotEmpty { - eventList?.append(eventName) - } + return events.compactMap { dict in + dict["eventName"] } } - return eventList + return nil } public let destinationDefinition: DestinationDefinition? diff --git a/Sources/Classes/Core/SourceConfigDownload/SourceConfigDownload.swift b/Sources/Classes/Core/SourceConfigDownload/SourceConfigDownload.swift index 3fe8e3ef..6d4a75ae 100644 --- a/Sources/Classes/Core/SourceConfigDownload/SourceConfigDownload.swift +++ b/Sources/Classes/Core/SourceConfigDownload/SourceConfigDownload.swift @@ -10,12 +10,12 @@ import Foundation class SourceConfigDownload { private var downloader: SourceConfigDownloadWorkerType - var sourceConfig: ((SourceConfig) -> Void) = { _ in } + var sourceConfig: ((SourceConfig, NeedsDatabaseMigration) -> Void) = { _, _ in } init(downloader: SourceConfigDownloadWorkerType) { self.downloader = downloader - self.downloader.sourceConfig = { sourceConfig in - self.sourceConfig(sourceConfig) + self.downloader.sourceConfig = { sourceConfig, needsDatabaseMigration in + self.sourceConfig(sourceConfig, needsDatabaseMigration) } } } diff --git a/Sources/Classes/Core/SourceConfigDownload/SourceConfigDownloadWorker.swift b/Sources/Classes/Core/SourceConfigDownload/SourceConfigDownloadWorker.swift index 461cfdc9..a6e8bbad 100644 --- a/Sources/Classes/Core/SourceConfigDownload/SourceConfigDownloadWorker.swift +++ b/Sources/Classes/Core/SourceConfigDownload/SourceConfigDownloadWorker.swift @@ -8,20 +8,24 @@ import Foundation +typealias NeedsDatabaseMigration = Bool + protocol SourceConfigDownloadWorkerType { - var sourceConfig: ((SourceConfig) -> Void) { get set } + var sourceConfig: ((SourceConfig, NeedsDatabaseMigration) -> Void) { get set } } class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { - var sourceConfig: ((SourceConfig) -> Void) = { _ in } + var sourceConfig: ((SourceConfig, NeedsDatabaseMigration) -> Void) = { _, _ in } let sourceConfigDownloader: SourceConfigDownloaderType let downloadBlockers: DownloadUploadBlockersProtocol - let userDefaults: UserDefaultsWorkerType + let userDefaults: UserDefaultsWorkerProtocol let queue: DispatchQueue let logger: Logger let retryStrategy: DownloadUploadRetryStrategy + var cachedSourceConfig: SourceConfig? + @ReadWriteLock var readWorkItem: DispatchWorkItem? @@ -31,7 +35,7 @@ class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { init( sourceConfigDownloader: SourceConfigDownloaderType, downloadBlockers: DownloadUploadBlockersProtocol, - userDefaults: UserDefaultsWorkerType, + userDefaults: UserDefaultsWorkerProtocol, queue: DispatchQueue, logger: Logger, retryStrategy: DownloadUploadRetryStrategy @@ -45,7 +49,8 @@ class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { self.readWorkItem = DispatchWorkItem { [weak self] in guard let self = self else { return } if let sourceConfig: SourceConfig = userDefaults.read(.sourceConfig) { - self.sourceConfig(sourceConfig) + self.cachedSourceConfig = sourceConfig + self.sourceConfig(sourceConfig, false) } let blockersForDownload = downloadBlockers.get() if blockersForDownload.isEmpty { @@ -64,7 +69,7 @@ class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { if let sourceConfig = response.sourceConfig { self.retryStrategy.reset() self.logger.logDebug(.sourceConfigDownloadSuccess) - self.sourceConfig(sourceConfig) + self.sourceConfig(sourceConfig, self.needsMigration(freshSourceConfig: sourceConfig)) } let downloadStatus = response.status if downloadStatus.needsRetry { @@ -78,14 +83,7 @@ class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { return } if let error = downloadStatus.error { - switch error { - case .httpError(let statusCode): - self.logger.logError(.sourceConfigDownloadFailedWithStatusCode(statusCode)) - case .networkError(let error): - self.logger.logError(.sourceConfigDownloadFailedWithErrorDescription(error.localizedDescription)) - case .noResponse: - self.logger.logError(.noResponse) - } + self.logger.logError(.apiError(.sourceConfig, error)) } } self.downloadWorkItem = workItem @@ -99,3 +97,12 @@ class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { queue.asyncAfter(deadline: .now() + retryStrategy.current, execute: readWorkItem) } } + +extension SourceConfigDownloadWorker { + func needsMigration(freshSourceConfig: SourceConfig) -> Bool { + guard let cachedSourceConfig = cachedSourceConfig else { + return false + } + return freshSourceConfig.id == cachedSourceConfig.id + } +} diff --git a/Sources/Classes/Core/Storage/Database.swift b/Sources/Classes/Core/Storage/Database/Database.swift similarity index 85% rename from Sources/Classes/Core/Storage/Database.swift rename to Sources/Classes/Core/Storage/Database/Database.swift index a18684f6..ba7660e9 100644 --- a/Sources/Classes/Core/Storage/Database.swift +++ b/Sources/Classes/Core/Storage/Database/Database.swift @@ -74,4 +74,17 @@ public protocol Database { @discardableResult func column_text(_: OpaquePointer!, _ iCol: Int32) -> UnsafePointer! + + @discardableResult + func exec(_ sql: UnsafePointer!, + _ callback: (@convention(c) (UnsafeMutableRawPointer?, Int32, + UnsafeMutablePointer?>?, + UnsafeMutablePointer?>? + ) -> Int32)!, + _ arg: UnsafeMutableRawPointer!, + _ errmsg: UnsafeMutablePointer?>! + ) -> Int32 + + @discardableResult + func close() -> Int32 } diff --git a/Sources/Classes/Core/Storage/DefaultDatabase.swift b/Sources/Classes/Core/Storage/Database/SQLiteDatabase.swift similarity index 72% rename from Sources/Classes/Core/Storage/DefaultDatabase.swift rename to Sources/Classes/Core/Storage/Database/SQLiteDatabase.swift index f327557a..8f8bdb2c 100644 --- a/Sources/Classes/Core/Storage/DefaultDatabase.swift +++ b/Sources/Classes/Core/Storage/Database/SQLiteDatabase.swift @@ -1,5 +1,5 @@ // -// DefaultDatabase.swift +// SQLiteDatabase.swift // Rudder // // Created by Pallab Maiti on 11/01/24. @@ -9,7 +9,7 @@ import Foundation import SQLite3 -class DefaultDatabase: Database { +class SQLiteDatabase: Database { var path: URL var name: String private var database: OpaquePointer? @@ -54,4 +54,18 @@ class DefaultDatabase: Database { func column_text(_ pStmt: OpaquePointer!, _ iCol: Int32) -> UnsafePointer! { return sqlite3_column_text(pStmt, iCol) } + + func exec(_ sql: UnsafePointer!, + _ callback: (@convention(c) (UnsafeMutableRawPointer?, Int32, + UnsafeMutablePointer?>?, + UnsafeMutablePointer?>?) -> Int32)!, + _ arg: UnsafeMutableRawPointer!, + _ errmsg: UnsafeMutablePointer?>! + ) -> Int32 { + return sqlite3_exec(database, sql, callback, arg, errmsg) + } + + func close() -> Int32 { + return sqlite3_close_v2(database) + } } diff --git a/Sources/Classes/Core/Storage/Migration/StorageMigration.swift b/Sources/Classes/Core/Storage/Migration/StorageMigration.swift new file mode 100644 index 00000000..bf76d988 --- /dev/null +++ b/Sources/Classes/Core/Storage/Migration/StorageMigration.swift @@ -0,0 +1,21 @@ +// +// StorageMigration.swift +// Rudder +// +// Created by Pallab Maiti on 02/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +class StorageMigration { + let storageMigrator: StorageMigrator + + init(storageMigrator: StorageMigrator) { + self.storageMigrator = storageMigrator + } + + func migrate() throws { + try storageMigrator.migrate() + } +} diff --git a/Sources/Classes/Core/Storage/Migration/StorageMigrator.swift b/Sources/Classes/Core/Storage/Migration/StorageMigrator.swift new file mode 100644 index 00000000..7bd8372c --- /dev/null +++ b/Sources/Classes/Core/Storage/Migration/StorageMigrator.swift @@ -0,0 +1,40 @@ +// +// StorageMigratorV1V2.swift +// Rudder +// +// Created by Pallab Maiti on 02/02/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +protocol StorageMigrator { + var currentStorage: Storage { get set } + func migrate() throws +} + +class StorageMigratorV1V2: StorageMigrator { + let oldSQLiteStorage: SQLiteStorage + var currentStorage: Storage + + init(oldSQLiteStorage: SQLiteStorage, currentStorage: Storage) { + self.oldSQLiteStorage = oldSQLiteStorage + self.currentStorage = currentStorage + } + + func migrate() throws { + let databasePath = oldSQLiteStorage.database.path.appendingPathComponent(oldSQLiteStorage.database.name).path + guard FileManager.default.fileExists(atPath: databasePath) else { + throw StorageError.databaseNotExists + } + let result = oldSQLiteStorage.objects(limit: .max) + switch result { + case .success(let list): + list.forEach({ _ = currentStorage.save($0) }) + _ = oldSQLiteStorage.close() + try FileManager.default.removeItem(atPath: databasePath) + case .failure(let error): + throw error + } + } +} diff --git a/Sources/Classes/Core/Storage/DefaultStorage.swift b/Sources/Classes/Core/Storage/SQLiteStorage.swift similarity index 90% rename from Sources/Classes/Core/Storage/DefaultStorage.swift rename to Sources/Classes/Core/Storage/SQLiteStorage.swift index 7303bc38..c9ea6ab2 100644 --- a/Sources/Classes/Core/Storage/DefaultStorage.swift +++ b/Sources/Classes/Core/Storage/SQLiteStorage.swift @@ -8,16 +8,17 @@ import Foundation -class DefaultStorage: Storage { +class SQLiteStorage: Storage { let database: Database let logger: Logger - init(database: Database, logger: Logger) { + required init(database: Database, logger: Logger) { self.database = database self.logger = logger } + @discardableResult func open() -> Results { if database.open() == DatabaseError.OK { return createTable() @@ -26,9 +27,9 @@ class DefaultStorage: Storage { } } - func createTable() -> Results { + private func createTable() -> Results { var createTableStatement: OpaquePointer? - let createTableString = "CREATE TABLE IF NOT EXISTS events( id INTEGER PRIMARY KEY AUTOINCREMENT, message TEXT NOT NULL, updated INTEGER NOT NULL);" + let createTableString = "CREATE TABLE IF NOT EXISTS events(id INTEGER PRIMARY KEY AUTOINCREMENT, message TEXT NOT NULL, updated INTEGER NOT NULL);" let result: Results logger.logDebug(.sqlStatement(createTableString)) if database.prepare(createTableString, -1, &createTableStatement, nil) == DatabaseError.OK { @@ -48,13 +49,14 @@ class DefaultStorage: Storage { return result } + @discardableResult func save(_ object: StorageMessage) -> Results { let insertStatementString = "INSERT INTO events (message, updated) VALUES (?, ?);" var insertStatement: OpaquePointer? let result: Results if database.prepare(insertStatementString, -1, &insertStatement, nil) == DatabaseError.OK { database.bind_text(insertStatement, 1, ((object.message.replacingOccurrences(of: "'", with: "''")) as NSString).utf8String, -1, nil) - database.bind_int(insertStatement, 2, Int32(Utility.getTimeStamp())) + database.bind_int(insertStatement, 2, Int32(object.updated)) logger.logDebug(.sqlStatement(insertStatementString)) if database.step(insertStatement) == DatabaseError.DONE { result = .success(true) @@ -72,6 +74,7 @@ class DefaultStorage: Storage { return result } + @discardableResult func objects(limit: Int) -> Results<[StorageMessage]> { var queryStatement: OpaquePointer? let result: Results<[StorageMessage]> @@ -84,7 +87,8 @@ class DefaultStorage: Storage { guard let message = database.column_text(queryStatement, 1) else { continue } - let messageEntity = StorageMessage(id: messageId, message: String(cString: message)) + let updated = database.column_int(queryStatement, 2) + let messageEntity = StorageMessage(id: messageId, message: String(cString: message), updated: Int(updated)) messageList.append(messageEntity) } result = .success(messageList) @@ -97,6 +101,7 @@ class DefaultStorage: Storage { return result } + @discardableResult func delete(_ objects: [StorageMessage]) -> Results { var deleteStatement: OpaquePointer? let messageIds = objects.compactMap({ $0.id }) @@ -120,6 +125,7 @@ class DefaultStorage: Storage { return result } + @discardableResult func deleteAll() -> Results { var deleteStatement: OpaquePointer? let deleteStatementString = "DELETE FROM 'events'" @@ -162,4 +168,9 @@ class DefaultStorage: Storage { database.finalize(queryStatement) return result } + + @discardableResult + func close() -> Results { + return .success(database.close() == DatabaseError.OK) + } } diff --git a/Sources/Classes/Core/Storage/Storage.swift b/Sources/Classes/Core/Storage/Storage.swift index dcc1fcb7..7da17c8d 100644 --- a/Sources/Classes/Core/Storage/Storage.swift +++ b/Sources/Classes/Core/Storage/Storage.swift @@ -10,13 +10,16 @@ import Foundation public typealias Results = Result -public enum StorageError: Error { +public enum StorageError: Error, Equatable { case storageError(String) + case databaseNotExists var description: String { switch self { case .storageError(let string): return string + case .databaseNotExists: + return "Old database not exists, hence no migration needed" } } } @@ -28,4 +31,5 @@ public protocol Storage { @discardableResult func delete(_ objects: [StorageMessage]) -> Results @discardableResult func deleteAll() -> Results func count() -> Results + @discardableResult func close() -> Results } diff --git a/Sources/Classes/Core/Storage/StorageWorker.swift b/Sources/Classes/Core/Storage/StorageWorker.swift index 69f425f2..2e1f6b6c 100644 --- a/Sources/Classes/Core/Storage/StorageWorker.swift +++ b/Sources/Classes/Core/Storage/StorageWorker.swift @@ -9,16 +9,17 @@ import Foundation import SQLite3 -protocol StorageWorker { +public protocol StorageWorkerProtocol { func open() func saveMessage(_ message: StorageMessage) func clearMessages(_ messages: [StorageMessage]) func fetchMessages(limit: Int) -> [StorageMessage]? func getMessageCount() -> Int? func clearAll() + func close() } -class DefaultStorageWorker: StorageWorker { +class StorageWorker: StorageWorkerProtocol { let storage: Storage let queue: DispatchQueue @@ -61,4 +62,10 @@ class DefaultStorageWorker: StorageWorker { _ = storage.deleteAll() } } + + func close() { + queue.sync { + _ = storage.close() + } + } }