diff --git a/.gitignore b/.gitignore index 2bb5b2fd..b629128f 100644 --- a/.gitignore +++ b/.gitignore @@ -97,4 +97,4 @@ compile_commands.json GoogleService-Info.plist RudderConfig*.plist configuration.json -RudderCore/Tests/**/*.plist \ No newline at end of file +RudderCore/Tests/**/*.plist diff --git a/Examples/SampleApps/Swift-iOS/ViewController.swift b/Examples/SampleApps/Swift-iOS/ViewController.swift index 2f6144e3..24a2ddd0 100644 --- a/Examples/SampleApps/Swift-iOS/ViewController.swift +++ b/Examples/SampleApps/Swift-iOS/ViewController.swift @@ -146,23 +146,23 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { case 18: client1.setAdvertisingId("advertising_id_2") case 19: - let option = Option() + let option = GlobalOption() - option.putIntegration("key-5", isEnabled: true) - option.putIntegration("key-6", isEnabled: true) - option.putIntegration("key-7", isEnabled: true) - option.putIntegration("key-8", isEnabled: false) + option.putIntegrationStatus("key-5", isEnabled: true) + option.putIntegrationStatus("key-6", isEnabled: true) + option.putIntegrationStatus("key-7", isEnabled: true) + option.putIntegrationStatus("key-8", isEnabled: false) - client1.setOption(option) + client1.setGlobalOption(option) case 20: - let option = IdentifyOption() + let option = MessageOption() option.putExternalId("value-1", to: "key-1") option.putExternalId("value-2", to: "key-2") - option.putIntegration("key-5", isEnabled: true) - option.putIntegration("key-6", isEnabled: true) - option.putIntegration("key-7", isEnabled: false) - option.putIntegration("key-8", isEnabled: false) + option.putIntegrationStatus("key-5", isEnabled: true) + option.putIntegrationStatus("key-6", isEnabled: true) + option.putIntegrationStatus("key-7", isEnabled: false) + option.putIntegrationStatus("key-8", isEnabled: false) option.putCustomContext(["Key-01": "value-1"], for: "key-9") option.putCustomContext(["Key-02": "value-1"], for: "key-10") @@ -172,12 +172,12 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate { case 21: let option = MessageOption() .putCustomContext([:], for: "") - .putIntegration("", isEnabled: true) + .putIntegrationStatus("", isEnabled: true) - option.putIntegration("key-5", isEnabled: false) - option.putIntegration("key-6", isEnabled: true) - option.putIntegration("key-7", isEnabled: false) - option.putIntegration("key-8", isEnabled: true) + option.putIntegrationStatus("key-5", isEnabled: false) + option.putIntegrationStatus("key-6", isEnabled: true) + option.putIntegrationStatus("key-7", isEnabled: false) + option.putIntegrationStatus("key-8", isEnabled: true) option.putCustomContext(["Key-01": "value-1"], for: "key-9") option.putCustomContext(["Key-02": "value-2"], for: "key-10") diff --git a/Rudder.xcodeproj/project.pbxproj b/Rudder.xcodeproj/project.pbxproj index 15b0dc2a..7d7d807a 100644 --- a/Rudder.xcodeproj/project.pbxproj +++ b/Rudder.xcodeproj/project.pbxproj @@ -332,7 +332,6 @@ ED11FE072B78DA810007B019 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ED11FE062B78DA810007B019 /* Preview Assets.xcassets */; }; ED11FE1D2B78DB600007B019 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED11FE0D2B78DB4E0007B019 /* ViewController.swift */; }; ED11FE1E2B78DB650007B019 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ED11FE0E2B78DB4E0007B019 /* Assets.xcassets */; }; - ED11FE1F2B78DB680007B019 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = ED11FE0F2B78DB4E0007B019 /* GoogleService-Info.plist */; }; ED11FE202B78DB720007B019 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ED11FE102B78DB4E0007B019 /* LaunchScreen.storyboard */; }; ED11FE212B78DB760007B019 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ED11FE122B78DB4E0007B019 /* Main.storyboard */; }; ED11FE222B78DB790007B019 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED11FE142B78DB4E0007B019 /* AppDelegate.swift */; }; @@ -498,6 +497,14 @@ ED11FFF32B7932150007B019 /* RudderConfig_1.plist in Resources */ = {isa = PBXBuildFile; fileRef = ED11FE1A2B78DB4E0007B019 /* RudderConfig_1.plist */; }; ED11FFF72B7A0ED60007B019 /* RudderInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED11FA0F2B78B1790007B019 /* RudderInternal.framework */; }; ED11FFFC2B7A0EE10007B019 /* RudderInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED11FA1B2B78B18F0007B019 /* RudderInternal.framework */; }; + F6D7A76F2BBEDD160086DFA6 /* DataResidency.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D7A76E2BBEDD160086DFA6 /* DataResidency.swift */; }; + F6D7A7702BBEDD160086DFA6 /* DataResidency.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D7A76E2BBEDD160086DFA6 /* DataResidency.swift */; }; + F6D7A7712BBEDD160086DFA6 /* DataResidency.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D7A76E2BBEDD160086DFA6 /* DataResidency.swift */; }; + F6D7A7722BBEDD160086DFA6 /* DataResidency.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D7A76E2BBEDD160086DFA6 /* DataResidency.swift */; }; + F6D7A7732BBEE2810086DFA6 /* Rudder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED11F8382B78A8BC0007B019 /* Rudder.framework */; }; + F6D7A7742BBEE2810086DFA6 /* Rudder.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = ED11F8382B78A8BC0007B019 /* Rudder.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + F6D7A7772BBEE2810086DFA6 /* RudderInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED11FA0F2B78B1790007B019 /* RudderInternal.framework */; }; + F6D7A7782BBEE2810086DFA6 /* RudderInternal.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = ED11FA0F2B78B1790007B019 /* RudderInternal.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -648,6 +655,20 @@ remoteGlobalIDString = ED11FA1A2B78B18F0007B019; remoteInfo = "RudderInternal-macOS"; }; + F6D7A7752BBEE2810086DFA6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ED11F82F2B78A8BC0007B019 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ED11F8372B78A8BC0007B019; + remoteInfo = "Rudder-iOS"; + }; + F6D7A7792BBEE2810086DFA6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ED11F82F2B78A8BC0007B019 /* Project object */; + proxyType = 1; + remoteGlobalIDString = ED11FA0E2B78B1780007B019; + remoteInfo = "RudderInternal-iOS"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -687,6 +708,18 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + F6D7A77B2BBEE2810086DFA6 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + F6D7A7742BBEE2810086DFA6 /* Rudder.framework in Embed Frameworks */, + F6D7A7782BBEE2810086DFA6 /* RudderInternal.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -782,7 +815,6 @@ ED11FE062B78DA810007B019 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; ED11FE0D2B78DB4E0007B019 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; ED11FE0E2B78DB4E0007B019 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - ED11FE0F2B78DB4E0007B019 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; ED11FE112B78DB4E0007B019 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; ED11FE132B78DB4E0007B019 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; ED11FE142B78DB4E0007B019 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -851,6 +883,7 @@ ED11FFD32B7929D70007B019 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; ED11FFD62B7929D70007B019 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; ED11FFD82B7929D70007B019 /* Swift_macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swift_macOS.entitlements; sourceTree = ""; }; + F6D7A76E2BBEDD160086DFA6 /* DataResidency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataResidency.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -927,6 +960,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F6D7A7732BBEE2810086DFA6 /* Rudder.framework in Frameworks */, + F6D7A7772BBEE2810086DFA6 /* RudderInternal.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1253,6 +1288,7 @@ children = ( ED11FC7A2B78CB2A0007B019 /* SessionStorage.swift */, ED11FC792B78CB2A0007B019 /* UserDefaultsWorker.swift */, + F6D7A76E2BBEDD160086DFA6 /* DataResidency.swift */, ); path = Helpers; sourceTree = ""; @@ -1303,7 +1339,6 @@ children = ( ED11FE142B78DB4E0007B019 /* AppDelegate.swift */, ED11FE0E2B78DB4E0007B019 /* Assets.xcassets */, - ED11FE0F2B78DB4E0007B019 /* GoogleService-Info.plist */, ED11FE152B78DB4E0007B019 /* Info.plist */, ED11FE102B78DB4E0007B019 /* LaunchScreen.storyboard */, ED11FE122B78DB4E0007B019 /* Main.storyboard */, @@ -1759,10 +1794,13 @@ ED11FDF92B78DA800007B019 /* Sources */, ED11FDFA2B78DA800007B019 /* Frameworks */, ED11FDFB2B78DA800007B019 /* Resources */, + F6D7A77B2BBEE2810086DFA6 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + F6D7A7762BBEE2810086DFA6 /* PBXTargetDependency */, + F6D7A77A2BBEE2810086DFA6 /* PBXTargetDependency */, ); name = "SwiftUI-iOS"; productName = "SwiftUI-iOS"; @@ -2075,7 +2113,6 @@ ED11FE1E2B78DB650007B019 /* Assets.xcassets in Resources */, ED11FE272B78DB980007B019 /* RudderConfig_1.plist in Resources */, ED11FE252B78DB980007B019 /* RudderConfig_2.plist in Resources */, - ED11FE1F2B78DB680007B019 /* GoogleService-Info.plist in Resources */, ED11FE212B78DB760007B019 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2244,6 +2281,7 @@ ED11FC942B78CB2A0007B019 /* SourceConfigDownloadWorker.swift in Sources */, ED11FD402B78CB2B0007B019 /* DataUploadWorker.swift in Sources */, ED11FC7C2B78CB2A0007B019 /* SourceConfigDownload.swift in Sources */, + F6D7A76F2BBEDD160086DFA6 /* DataResidency.swift in Sources */, ED11FCDC2B78CB2B0007B019 /* PhoneApplicationState.swift in Sources */, ED11FD382B78CB2B0007B019 /* DataUpload.swift in Sources */, ED11FD082B78CB2B0007B019 /* SQLiteDatabase.swift in Sources */, @@ -2312,6 +2350,7 @@ ED11FC952B78CB2A0007B019 /* SourceConfigDownloadWorker.swift in Sources */, ED11FD412B78CB2B0007B019 /* DataUploadWorker.swift in Sources */, ED11FC7D2B78CB2A0007B019 /* SourceConfigDownload.swift in Sources */, + F6D7A7702BBEDD160086DFA6 /* DataResidency.swift in Sources */, ED11FCDD2B78CB2B0007B019 /* PhoneApplicationState.swift in Sources */, ED11FD392B78CB2B0007B019 /* DataUpload.swift in Sources */, ED11FD092B78CB2B0007B019 /* SQLiteDatabase.swift in Sources */, @@ -2380,6 +2419,7 @@ ED11FC962B78CB2A0007B019 /* SourceConfigDownloadWorker.swift in Sources */, ED11FD422B78CB2B0007B019 /* DataUploadWorker.swift in Sources */, ED11FC7E2B78CB2A0007B019 /* SourceConfigDownload.swift in Sources */, + F6D7A7712BBEDD160086DFA6 /* DataResidency.swift in Sources */, ED11FCDE2B78CB2B0007B019 /* PhoneApplicationState.swift in Sources */, ED11FD3A2B78CB2B0007B019 /* DataUpload.swift in Sources */, ED11FD0A2B78CB2B0007B019 /* SQLiteDatabase.swift in Sources */, @@ -2448,6 +2488,7 @@ ED11FC972B78CB2A0007B019 /* SourceConfigDownloadWorker.swift in Sources */, ED11FD432B78CB2B0007B019 /* DataUploadWorker.swift in Sources */, ED11FC7F2B78CB2A0007B019 /* SourceConfigDownload.swift in Sources */, + F6D7A7722BBEDD160086DFA6 /* DataResidency.swift in Sources */, ED11FCDF2B78CB2B0007B019 /* PhoneApplicationState.swift in Sources */, ED11FD3B2B78CB2B0007B019 /* DataUpload.swift in Sources */, ED11FD0B2B78CB2B0007B019 /* SQLiteDatabase.swift in Sources */, @@ -2903,6 +2944,16 @@ target = ED11FA1A2B78B18F0007B019 /* RudderInternal-macOS */; targetProxy = ED11FFFE2B7A0EE10007B019 /* PBXContainerItemProxy */; }; + F6D7A7762BBEE2810086DFA6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ED11F8372B78A8BC0007B019 /* Rudder-iOS */; + targetProxy = F6D7A7752BBEE2810086DFA6 /* PBXContainerItemProxy */; + }; + F6D7A77A2BBEE2810086DFA6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = ED11FA0E2B78B1780007B019 /* RudderInternal-iOS */; + targetProxy = F6D7A7792BBEE2810086DFA6 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -3807,7 +3858,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = WPX9KRKA8B; + DEVELOPMENT_TEAM = X54342W87P; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -3824,7 +3875,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = WPX9KRKA8B; + DEVELOPMENT_TEAM = X54342W87P; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; diff --git a/RudderCore/Sources/Core/Attributes/Configuration.swift b/RudderCore/Sources/Core/Attributes/Configuration.swift index c8ed2c06..e7b8af1a 100644 --- a/RudderCore/Sources/Core/Attributes/Configuration.swift +++ b/RudderCore/Sources/Core/Attributes/Configuration.swift @@ -8,6 +8,11 @@ import Foundation +public enum DataResidencyServer { + case US + case EU +} + enum ConfigValidationError: Error { case flushQueueSize case dbCountThreshold @@ -87,6 +92,11 @@ public class Configuration { return _gzipEnabled } + private var _dataResidencyServer: DataResidencyServer = Constants.residencyServer.default + public var dataResidencyServer: DataResidencyServer { + return _dataResidencyServer + } + private var _flushPolicies = [FlushPolicy]() public var flushPolicies: [FlushPolicy] { _flushPolicies @@ -106,7 +116,7 @@ public class Configuration { public var logger: LoggerProtocol? { _logger } - + var configValidationErrorList = [ConfigValidationError]() required public init?(writeKey: String, dataPlaneURL: String) { @@ -191,6 +201,12 @@ public class Configuration { return self } + @discardableResult + public func dataResidencyServer(_ dataResidencyServer: DataResidencyServer) -> Configuration { + _dataResidencyServer = dataResidencyServer + return self + } + @discardableResult public func flushPolicies(_ flushPolicies: [FlushPolicy]) -> Configuration { if !flushPolicies.isEmpty { diff --git a/RudderCore/Sources/Core/Attributes/Context.swift b/RudderCore/Sources/Core/Attributes/Context.swift index f1378b17..dd74e6d4 100644 --- a/RudderCore/Sources/Core/Attributes/Context.swift +++ b/RudderCore/Sources/Core/Attributes/Context.swift @@ -139,8 +139,8 @@ public struct Context: Codable { _traits?.dictionaryValue } - private let _externalIds: [[String: String]]? - public var externalIds: [[String: String]]? { + private let _externalIds: [ExternalId]? + public var externalIds: [ExternalId]? { _externalIds } @@ -208,3 +208,9 @@ public extension Encodable { return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] } } } + +public extension [ExternalId] { + var array: [[String:Any]?]? { + self.map{$0.dictionary} + } +} diff --git a/RudderCore/Sources/Core/Attributes/Message.swift b/RudderCore/Sources/Core/Attributes/Message.swift index a27e0312..04a95b4d 100644 --- a/RudderCore/Sources/Core/Attributes/Message.swift +++ b/RudderCore/Sources/Core/Attributes/Message.swift @@ -91,7 +91,7 @@ public struct IdentifyMessage: Message { dictionary["event"] = "identify" } - init(userId: String, traits: IdentifyTraits? = nil, option: IdentifyOptionType? = nil) { + init(userId: String, traits: IdentifyTraits? = nil, option: MessageOptionType? = nil) { self.userId = userId self.traits = traits self.option = option diff --git a/RudderCore/Sources/Core/Attributes/Option.swift b/RudderCore/Sources/Core/Attributes/Option.swift index e7674bb6..369d7620 100644 --- a/RudderCore/Sources/Core/Attributes/Option.swift +++ b/RudderCore/Sources/Core/Attributes/Option.swift @@ -8,60 +8,71 @@ import Foundation -public protocol OptionType { - var integrations: [String: Bool] { get } + +public struct ExternalId: Codable { + let type: String + let id: String } -public protocol MessageOptionType: OptionType { +public protocol GlobalOptionType { + /** + ``integrationsStatus`` object is used to enable/disable sending events to any destination from the Client Side + Eg: `["Amplitude" : true, "Adjust": false]` + The above example would mean that the destination `Amplitude` is enabled and the destination `Adjust` is disabled. + If the above example value is passed as part of ``GlobalOptionType``, it would be applied to all the events made in a session of SDK initialization, unless overridden by the ``MessageOptionType`` at an event level. + + If ``integrationsStatus`` is passed as part of ``MessageOptionType``, it would be applied only for that particular message. + */ + + var integrationsStatus: [String: Bool]? { get } + + /** + ``customContexts`` helps you to set an object which can be added to `context` object of the payload generated for all the types of events. If this is passed as part of ``GlobalOptionType`` this would be applied to all the events made in a session of SDK initialization, unless overridden by the ``MessageOptionType`` at an event level. + + If ``customContexts`` is passed as part of ``MessageOptionType``, it would be applied only for that particular message. + */ var customContexts: [String: Any]? { get } } -public protocol IdentifyOptionType: MessageOptionType { - var externalIds: [[String: String]]? { get } +public protocol MessageOptionType: GlobalOptionType { + /** + ``externalIds`` are sometimes used to add new identifiers to the current user which can be used by the Cloud mode destinations. + - These values can be passed only via an event level API and cannot be passed with SDK initialization. + - If ``externalIds`` are passed as part of `Identify` event, they are persisted and attached along with every event made until the user logs out by calling `Reset` + - If ``externalIds`` are passed as part of any other event, then they are applied only for that particular event, and if any persisted externalIds exists from any previous `Identify` call, they are overriden just for the current event. + */ + var externalIds: [ExternalId]? { get } } -public class Option: OptionType { - private var _integrations: [String: Bool] = [String: Bool]() - public var integrations: [String: Bool] { - _integrations - } +public class GlobalOption: GlobalOptionType { - public init() { } + public init() {} - @discardableResult - public func putIntegration(_ name: String, isEnabled: Bool) -> Option { - guard name.isNotEmpty else { - return self - } - _integrations[name] = isEnabled - return self - } -} - -public class MessageOption: OptionType, MessageOptionType { - private var _integrations: [String: Bool] = [String: Bool]() + private var _integrationsStatus: [String: Bool]? private var _customContexts: [String: Any]? - public var integrations: [String: Bool] { - _integrations + public var integrationsStatus: [String: Bool]? { + _integrationsStatus } + public var customContexts: [String: Any]? { _customContexts } - public init() { } - @discardableResult - public func putIntegration(_ name: String, isEnabled: Bool) -> MessageOption { + public func putIntegrationStatus(_ name: String, isEnabled: Bool) -> Self { guard name.isNotEmpty else { return self } - _integrations[name] = isEnabled + if _integrationsStatus == nil { + _integrationsStatus = [String: Bool]() + } + _integrationsStatus?[name] = isEnabled return self } @discardableResult - public func putCustomContext(_ context: [String: Any], for key: String) -> MessageOption { + public func putCustomContext(_ context: [String: Any], for key: String) -> Self { guard key.isNotEmpty else { return self } @@ -73,61 +84,45 @@ public class MessageOption: OptionType, MessageOptionType { } } -public class IdentifyOption: OptionType, MessageOptionType, IdentifyOptionType { - private var _integrations: [String: Bool] = [String: Bool]() - private var _customContexts: [String: Any]? - private var _externalIds: [[String: String]]? +public class MessageOption: GlobalOption, MessageOptionType { - public var integrations: [String: Bool] { - _integrations - } - public var customContexts: [String: Any]? { - _customContexts - } + public override init() {} + + private var _externalIds: [ExternalId]? - public var externalIds: [[String: String]]? { + public var externalIds: [ExternalId]? { _externalIds } - - public init() { } - + @discardableResult - public func putIntegration(_ name: String, isEnabled: Bool) -> IdentifyOption { - guard name.isNotEmpty else { + public func putExternalId(_ id: String, to type: String) -> MessageOption { + guard type.isNotEmpty, id.isNotEmpty else { return self } - _integrations[name] = isEnabled + if _externalIds == nil { + _externalIds = [ExternalId]() + } + + _externalIds?.add(ExternalId(type: type, id: id)) return self } - - @discardableResult - public func putCustomContext(_ context: [String: Any], for key: String) -> IdentifyOption { - guard key.isNotEmpty else { - return self - } - if _customContexts == nil { - _customContexts = [String: Any]() +} + + +extension [ExternalId] { + mutating func add(_ externalId: ExternalId) { + if let index = self.firstIndex(where: { $0.type == externalId.type }) { + // Update the externalId, if an externalId with the same type already exists + self[index] = externalId + } else { + // Append the new externalId to the array + self.append(externalId) } - _customContexts?[key] = context - return self } - @discardableResult - public func putExternalId(_ id: String, to type: String) -> IdentifyOption { - guard type.isNotEmpty, id.isNotEmpty else { - return self - } - if _externalIds == nil { - _externalIds = [[String: String]]() + mutating func add(_ externalIds: [ExternalId]) { + for externalId in externalIds { + add(externalId) } - if let index = _externalIds?.firstIndex(where: { dict in - return dict["type"] == type - }) { - _externalIds?[index]["id"] = id - } else { - let dict = ["type": type, "id": id] - _externalIds?.append(dict) - } - return self } } diff --git a/RudderCore/Sources/Core/Common/Constants.swift b/RudderCore/Sources/Core/Common/Constants.swift index 4a185a0b..1f701651 100644 --- a/RudderCore/Sources/Core/Common/Constants.swift +++ b/RudderCore/Sources/Core/Common/Constants.swift @@ -20,6 +20,7 @@ public class Constants { static let recordScreenViews = RecordScreenViews() static let autoSessionTracking = AutoSessionTracking() static let gzipEnabled = GzipEnabled() + static let residencyServer = ResidencyServer() } protocol DefaultValue { @@ -83,3 +84,7 @@ struct AutoSessionTracking: DefaultValue { struct GzipEnabled: DefaultValue { var `default`: Bool = true } + +struct ResidencyServer: DefaultValue { + var `default`: DataResidencyServer = .US +} diff --git a/RudderCore/Sources/Core/Helpers/DataResidency.swift b/RudderCore/Sources/Core/Helpers/DataResidency.swift new file mode 100644 index 00000000..fdf8dbdc --- /dev/null +++ b/RudderCore/Sources/Core/Helpers/DataResidency.swift @@ -0,0 +1,41 @@ +// +// DataResidency.swift +// Rudder +// +// Created by Pallab Maiti on 23/01/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation + +protocol DataResidencyType { + var dataPlaneUrl: String? { get } +} + +class DataResidency: DataResidencyType { + var dataPlaneUrl: String? { + var dataPlanes: [SourceConfig.Source.DataPlanes.DataPlane]? + if dataResidencyServer == .EU { + dataPlanes = sourceConfig.source?.dataPlanes?.eu + } + + if dataPlanes == nil { + dataPlanes = sourceConfig.source?.dataPlanes?.us + } + + let dataPlane = dataPlanes?.filter({ $0.default }) + + if let urls = dataPlane?.compactMap({ $0.url }), !urls.isEmpty { + return urls.first + } + return nil + } + + let dataResidencyServer: DataResidencyServer + let sourceConfig: SourceConfig + + init(dataResidencyServer: DataResidencyServer, sourceConfig: SourceConfig) { + self.dataResidencyServer = dataResidencyServer + self.sourceConfig = sourceConfig + } +} diff --git a/RudderCore/Sources/Core/Helpers/SessionStorage.swift b/RudderCore/Sources/Core/Helpers/SessionStorage.swift index 60fca0a4..e31b8274 100644 --- a/RudderCore/Sources/Core/Helpers/SessionStorage.swift +++ b/RudderCore/Sources/Core/Helpers/SessionStorage.swift @@ -13,7 +13,7 @@ public enum SessionStorageKeys: String { case deviceToken case advertisingId case appTrackingConsent - case defaultOption + case globalOption case context } @@ -26,7 +26,7 @@ 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 globalOption: GlobalOptionType? @ReadWriteLock private var context: Context? func write(_ key: SessionStorageKeys, value: T?) { @@ -37,11 +37,16 @@ class SessionStorage: SessionStorageProtocol { advertisingId = value as? String case .appTrackingConsent: appTrackingConsent = value as? AppTrackingConsent - case .defaultOption: - defaultOption = value as? Option + case .globalOption: + globalOption = value as? GlobalOptionType case .context: - if let value = value as? MessageContext, let data = try? JSONSerialization.data(withJSONObject: value) { - context = try? JSONDecoder().decode(Context.self, from: data) + if let value = value as? MessageContext { + do { + let data = try JSONSerialization.data(withJSONObject: value) + context = try JSONDecoder().decode(Context.self, from: data) + } catch { + print(error) + } } } } @@ -55,8 +60,8 @@ class SessionStorage: SessionStorageProtocol { result = advertisingId as? T case .appTrackingConsent: result = appTrackingConsent as? T - case .defaultOption: - result = defaultOption as? T + case .globalOption: + result = globalOption as? T case .context: result = context as? T } diff --git a/RudderCore/Sources/Core/Helpers/UserDefaultsWorker.swift b/RudderCore/Sources/Core/Helpers/UserDefaultsWorker.swift index f57ac85e..91287a3f 100644 --- a/RudderCore/Sources/Core/Helpers/UserDefaultsWorker.swift +++ b/RudderCore/Sources/Core/Helpers/UserDefaultsWorker.swift @@ -13,6 +13,7 @@ public enum UserDefaultsKeys: String { case traits = "rs_traits" case anonymousId = "rs_anonymous_id" case sourceConfig = "rs_server_config" + case legacySourceConfig = "rl_server_config" case optStatus = "rs_opt_status" case optInTime = "rs_opt_in_time" case optOutTime = "rs_opt_out_time" diff --git a/RudderCore/Sources/Core/Logger/LogMessages.swift b/RudderCore/Sources/Core/Logger/LogMessages.swift index fd7aa92d..d003d79a 100644 --- a/RudderCore/Sources/Core/Logger/LogMessages.swift +++ b/RudderCore/Sources/Core/Logger/LogMessages.swift @@ -57,7 +57,9 @@ enum LogMessages { case eventFiltered case storageMigrationFailed(StorageError) case storageMigrationSuccess - case oldDatabaseNotExists + case legacyDatabaseDoesNotExists + case storageMigrationFailedToReadSourceConfig + case failedToDeleteLegacyDatabase(String) case apiError(API, APIError) case internalErrors(InternalErrors) @@ -130,9 +132,13 @@ enum LogMessages { 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" + return "Successfully migrated data from legacy storage and deleted the legacy database." + case .legacyDatabaseDoesNotExists: + return "Legacy database does not exists, hence no migration needed" + case .storageMigrationFailedToReadSourceConfig: + return "Legacy database exists, but failed to read legacy SourceConfig, so cannot migrate data, hence deleting the legacy database" + case .failedToDeleteLegacyDatabase(let reason): + return "Failed to delete legacy database due to \(reason)" case .apiError(let api, let error): switch error { case .httpError(let statusCode): diff --git a/RudderCore/Sources/Core/ObjC/ObjCRSClient.swift b/RudderCore/Sources/Core/ObjC/ObjCRSClient.swift new file mode 100644 index 00000000..e3ea35d1 --- /dev/null +++ b/RudderCore/Sources/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/RudderCore/Sources/Core/Plugins/ContextPlugin.swift b/RudderCore/Sources/Core/Plugins/ContextPlugin.swift index b6734d13..3851c060 100644 --- a/RudderCore/Sources/Core/Plugins/ContextPlugin.swift +++ b/RudderCore/Sources/Core/Plugins/ContextPlugin.swift @@ -66,7 +66,7 @@ class ContextPlugin: Plugin { // timezone context["timezone"] = Context.timezone() } - + internal func insertDynamicPlatformContextData(context: inout [String: Any]) { // network connectivity context["network"] = Context.NetworkInfo().dictionary @@ -85,26 +85,46 @@ class ContextPlugin: Plugin { } func insertDynamicOptionData(message: Message, context: inout [String: Any]) { - // 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]] = userDefaultsWorker?.read(.externalId) { - contextExternalIds.append(contentsOf: externalIds) + insertExternalIds(message: message, context: &context) + insertCustomContext(message: message, context: &context) + } + + func insertExternalIds(message: Message, context: inout [String: Any]) { + var mergedExternalIds = [ExternalId]() + /// Merging the externalIds from the persistence if there were any, as a result of previous identify calls. + if let externalIdsFromPersistence: [ExternalId] = userDefaultsWorker?.read(.externalId) { + mergedExternalIds.add(externalIdsFromPersistence) + } + /// Merging the externalIds from the `option` object of the current message if it is not an identify, if any duplicates found, we will override it. + /// We are not merging the message level `externalIds` in case of Identify Message, because they are already written to the UserDefaults. + if message.type != .identify, let option = message.option as? MessageOption, let externalIdsFromMessage = option.externalIds { + mergedExternalIds.add(externalIdsFromMessage) } - - if let option = message.option { - // We will merge the external ids for other event calls - if let option = option as? IdentifyOption, let externalIds = option.externalIds { - contextExternalIds.append(contentsOf: externalIds) + /// Setting the merged externalIds into the context object. + if !mergedExternalIds.isEmpty { + context["externalId"] = mergedExternalIds.array + } + } + + func insertCustomContext(message: Message, context: inout [String: Any]) { + var mergedCustomContexts = [String: Any]() + /// Merging the custom context from the `defaultOption` object passed while initializing the SDK + if let globalOption: GlobalOptionType = client?.sessionStorage.read(.globalOption), let globalCustomContexts = globalOption.customContexts { + mergedCustomContexts.merge(globalCustomContexts) { _, incoming in + incoming } - if let customContexts = option.customContexts { - for (key, value) in customContexts { - context[key] = value - } + } + /// Merging the custom context from the `option` object of the current message, if any duplicate keys found, we will override it. + if let option = message.option as? MessageOption, let customContextsFromMessage = option.customContexts { + mergedCustomContexts.merge(customContextsFromMessage) { _, incoming in + incoming } } - if !contextExternalIds.isEmpty { - context["externalId"] = contextExternalIds + /// Setting the merged custom context into the context object. + if !mergedCustomContexts.isEmpty { + for (key, value) in mergedCustomContexts { + context[key] = value + } } } diff --git a/RudderCore/Sources/Core/Plugins/IntegrationPlugin.swift b/RudderCore/Sources/Core/Plugins/IntegrationPlugin.swift index 83f6173c..434ff942 100644 --- a/RudderCore/Sources/Core/Plugins/IntegrationPlugin.swift +++ b/RudderCore/Sources/Core/Plugins/IntegrationPlugin.swift @@ -16,16 +16,20 @@ class IntegrationPlugin: Plugin { func process(message: T?) -> T? where T: Message { guard var workingMessage = message else { return message } - let messageIntegrations = workingMessage.option?.integrations ?? [:] - let globalOption: Option? = client?.sessionStorage.read(.defaultOption) - let globalOptionIntegrations = globalOption?.integrations ?? [:] + /// Retrieving the integrations status passed using `globalOption` object passed while initializing the SDK + let globalOption: GlobalOptionType? = client?.sessionStorage.read(.globalOption) + let globalOptionIntegrationsStatus = globalOption?.integrationsStatus ?? [:] - var integrations = messageIntegrations.merging(globalOptionIntegrations) { (current, _) in current } + /// Retrieving the integrations status passed using `option` object from the current message. + let messageIntegrationsStatus = workingMessage.option?.integrationsStatus ?? [:] - if integrations["All"] == nil { - integrations["All"] = true + /// Merging the integrations status from global `option` object with the ones from the message level `option` object and if there are any duplicate integrations in both the `option` objects, we are considering the integration status passed from `message level option` object. + var mergedIntegrationsStatus = globalOptionIntegrationsStatus.merging(messageIntegrationsStatus) { (_, incoming) in incoming } + + if mergedIntegrationsStatus["All"] == nil { + mergedIntegrationsStatus["All"] = true } - workingMessage.integrations = integrations + workingMessage.integrations = mergedIntegrationsStatus return workingMessage } } diff --git a/RudderCore/Sources/Core/RudderCore.swift b/RudderCore/Sources/Core/RudderCore.swift index 9c5d887f..7e6ea028 100644 --- a/RudderCore/Sources/Core/RudderCore.swift +++ b/RudderCore/Sources/Core/RudderCore.swift @@ -9,6 +9,7 @@ import Foundation import RudderInternal + class RudderCore { let configuration: Configuration let storage: Storage @@ -18,7 +19,7 @@ class RudderCore { let sourceConfigDownloader: SourceConfigDownloaderType let logger: Logger let instanceName: String - var applicationSate: ApplicationState + var applicationSate: ApplicationState? let downloadUploadBlockers: DownloadUploadBlockers = DownloadUploadBlockers() let sessionStorage: SessionStorageProtocol = SessionStorage() var database: Database? @@ -47,14 +48,14 @@ class RudderCore { init( configuration: Configuration, instanceName: String, + globalOption: GlobalOptionType? = nil, database: Database? = nil, storage: Storage? = nil, userDefaults: UserDefaults? = nil, sourceConfigDownloader: SourceConfigDownloaderType? = nil, dataUploader: DataUploaderType? = nil, apiClient: APIClient? = nil, - applicationState: ApplicationState? = nil, - storageMigrator: StorageMigrator? = nil + applicationState: ApplicationState? = nil ) { self.configuration = configuration self.instanceName = instanceName @@ -90,6 +91,12 @@ class RudderCore { } else { self.userDefaultsWorker = UserDefaultsWorker(suiteName: "defaultUserDefaults".userDefaultsSuitName(instanceName), queue: userDefaultsQueue) } + + let userOptedOut: Bool = userDefaultsWorker.read(.optStatus) ?? false + if !userOptedOut, let globalOption = globalOption { + sessionStorage.write(.globalOption, value: globalOption) + } + self.serviceManager = ServiceManager( apiClient: apiClient ?? URLSessionClient( session: URLSession.defaultSession() @@ -107,27 +114,34 @@ class RudderCore { if !configuration.flushPolicies.isEmpty { self.flushPolicies.append(contentsOf: configuration.flushPolicies) } - self.storageMigrator = storageMigrator - self.applicationSate = ApplicationState.current( + + trackApplicationState() + observeNotifications() + fetchSourceConfig() + logConfigValidationErrors() + } + +} + + +extension RudderCore { + + private func trackApplicationState() { + applicationSate = ApplicationState.current( notificationCenter: NotificationCenter.default, userDefaultsWorker: self.userDefaultsWorker ) - self.applicationSate.observeNotifications() - self.applicationSate.trackApplicationStateMessage = { [weak self] applicationStateMessage in + applicationSate?.observeNotifications() + applicationSate?.trackApplicationStateMessage = { [weak self] applicationStateMessage in guard let self = self, self.configuration.trackLifecycleEvents else { return } self.track(applicationStateMessage.state.eventName, properties: applicationStateMessage.properties) } - self.applicationSate.refreshSessionIfNeeded = { [weak self] in + applicationSate?.refreshSessionIfNeeded = { [weak self] in guard let self = self, self.configuration.trackLifecycleEvents else { return } self.refreshSessionIfNeeded() } - observeNotifications() - fetchSourceConfig() - logConfigValidationErrors() } -} -extension RudderCore { private func observeNotifications() { NotificationName.allCases.forEach({ NotificationCenter.default.addObserver(self, selector: #selector(observe(notification:)), name: Notification.Name(notificationName: $0), object: nil) @@ -168,11 +182,9 @@ extension RudderCore { sourceConfigDownload = SourceConfigDownload(downloader: downloader) - sourceConfigDownload?.sourceConfig = { [weak self] sourceConfig, needsDatabaseMigration in + sourceConfigDownload?.sourceConfig = { [weak self] sourceConfig in guard let self = self else { return } - if needsDatabaseMigration { - self.migrateStorage() - } + self.migrateStorage(currentSourceConfig: sourceConfig) self.isEnable = sourceConfig.enabled self.updateSourceConfig(sourceConfig) self.userDefaultsWorker.write(.sourceConfig, value: sourceConfig) @@ -188,13 +200,18 @@ extension RudderCore { retryFactors: dataUploadRetryFactors ) + let dataResidency = DataResidency( + dataResidencyServer: self.configuration.dataResidencyServer, + sourceConfig: sourceConfig + ) + let dataUploader = self.dataUploader ?? DataUploader( serviceManager: self.serviceManager, anonymousId: self.userDefaultsWorker.read(.anonymousId) ?? "", gzipEnabled: self.configuration.gzipEnabled, - dataPlaneUrl: self.configuration.dataPlaneURL + dataPlaneUrl: dataResidency.dataPlaneUrl ?? self.configuration.dataPlaneURL ) - + let uploader = DataUploadWorker( dataUploader: dataUploader, dataUploadBlockers: self.downloadUploadBlockers, @@ -214,6 +231,9 @@ extension RudderCore { } else { if !sourceConfig.enabled { self.dataUpload?.cancel() + } else { + let dataResidency = DataResidency(dataResidencyServer: self.configuration.dataResidencyServer, sourceConfig: sourceConfig) + self.dataUploader?.updateDataPlaneUrl(dataResidency.dataPlaneUrl ?? self.configuration.dataPlaneURL) } } } @@ -223,33 +243,12 @@ extension RudderCore { 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)) - } - } + private func migrateStorage(currentSourceConfig: SourceConfig) { + let storageMigrator = StorageMigratorV1V2(currentStorage: storage, currentSourceConfig: currentSourceConfig, logger: logger) + let storageMigration = StorageMigration(storageMigrator: storageMigrator) + storageMigration.migrate() } -} -extension RudderCore { func updateSourceConfig(_ sourceConfig: SourceConfig) { pluginList.forEach { (_, value) in value.forEach { plugin in @@ -258,6 +257,7 @@ extension RudderCore { } } + #warning("add sync queue") func addPlugin(_ plugin: Plugin) { if var list = pluginList[plugin.type] { list.addPlugin(plugin) @@ -316,9 +316,10 @@ extension RudderCore { } } + extension RudderCore { - func track(_ eventName: String, properties: TrackProperties? = nil, option: MessageOption? = nil) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + func track(_ eventName: String, properties: TrackProperties? = nil, option: MessageOptionType? = nil) { + if isUserOptedOut() { logger.logDebug(.optOutAndEventDrop) return } @@ -330,8 +331,8 @@ extension RudderCore { process(message: message) } - func screen(_ screenName: String, category: String? = nil, properties: ScreenProperties? = nil, option: MessageOption? = nil) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + func screen(_ screenName: String, category: String? = nil, properties: ScreenProperties? = nil, option: MessageOptionType? = nil) { + if isUserOptedOut() { logger.logDebug(.optOutAndEventDrop) return } @@ -347,9 +348,9 @@ extension RudderCore { let message = ScreenMessage(title: screenName, category: category, properties: screenProperties, option: option) process(message: message) } - - func group(_ groupId: String, traits: [String: String]? = nil, option: MessageOption? = nil) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + + func group(_ groupId: String, traits: [String: String]? = nil, option: MessageOptionType? = nil) { + if isUserOptedOut() { logger.logDebug(.optOutAndEventDrop) return } @@ -360,9 +361,9 @@ extension RudderCore { let message = GroupMessage(groupId: groupId, traits: traits, option: option) process(message: message) } - - func alias(_ newId: String, option: MessageOption? = nil) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + + func alias(_ newId: String, option: MessageOptionType? = nil) { + if isUserOptedOut() { logger.logDebug(.optOutAndEventDrop) return } @@ -380,9 +381,9 @@ extension RudderCore { 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 = userDefaultsWorker.read(.optStatus), optOutStatus { + + func identify(_ userId: String, traits: IdentifyTraits? = nil, option: MessageOptionType? = nil) { + if isUserOptedOut() { logger.logDebug(.optOutAndEventDrop) return } @@ -390,14 +391,24 @@ extension RudderCore { logger.logWarning(.userIdNotEmpty) return } + + if let existingUserId: String = userDefaultsWorker.read(.userId), existingUserId != userId { + reset() + } + userDefaultsWorker.write(.userId, value: userId) if let traits = traits { userDefaultsWorker.write(.traits, value: try? JSON(traits)) } - if let externalIds = option?.externalIds { - userDefaultsWorker.write(.externalId, value: try? JSON(externalIds)) + /// merging the current externalIds with the persisted externalIds and in case of duplicates current externalIds will be considered + if var currrentExternalIds = option?.externalIds { + if var persistedExternalIds: [ExternalId] = userDefaultsWorker.read(.externalId) { + persistedExternalIds.add(currrentExternalIds) + currrentExternalIds = persistedExternalIds + } + userDefaultsWorker.write(.externalId, value: currrentExternalIds) } let message = IdentifyMessage(userId: userId, traits: traits, option: option) process(message: message) @@ -449,9 +460,10 @@ extension RudderCore { } } + extension RudderCore { var anonymousId: String? { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return nil } @@ -459,7 +471,7 @@ extension RudderCore { } var userId: String? { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return nil } @@ -467,7 +479,7 @@ extension RudderCore { } var context: Context? { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return nil } @@ -478,7 +490,7 @@ extension RudderCore { } var traits: IdentifyTraits? { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return nil } @@ -491,7 +503,7 @@ extension RudderCore { } var sessionId: Int? { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return nil } @@ -530,9 +542,10 @@ extension RudderCore { } } + extension RudderCore { func setAnonymousId(_ anonymousId: String) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return } @@ -542,17 +555,17 @@ extension RudderCore { } userDefaultsWorker.write(.anonymousId, value: anonymousId) } - - func setOption(_ option: Option) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + + func setGlobalOption(_ globalOption: GlobalOptionType) { + if isUserOptedOut() { logger.logDebug(.optOut) return } - sessionStorage.write(.defaultOption, value: option) + sessionStorage.write(.globalOption, value: globalOption) } func setDeviceToken(_ token: String) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return } @@ -564,7 +577,7 @@ extension RudderCore { } func setAdvertisingId(_ advertisingId: String) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return } @@ -578,7 +591,7 @@ extension RudderCore { } func setAppTrackingConsent(_ appTrackingConsent: AppTrackingConsent) { - if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus), optOutStatus { + if isUserOptedOut() { logger.logDebug(.optOut) return } @@ -589,7 +602,7 @@ extension RudderCore { userDefaultsWorker.write(.optStatus, value: status) logger.logDebug(.userOptOut(status)) } - + } extension RudderCore { @@ -598,6 +611,13 @@ extension RudderCore { userDefaultsWorker.remove(.externalId) userDefaultsWorker.remove(.userId) } + + func isUserOptedOut() -> Bool { + if let optOutStatus: Bool = userDefaultsWorker.read(.optStatus) { + return optOutStatus + } + return false + } } extension [Plugin] { diff --git a/RudderCore/Sources/Core/RudderProtocol.swift b/RudderCore/Sources/Core/RudderProtocol.swift index 4eacc325..259a443a 100644 --- a/RudderCore/Sources/Core/RudderProtocol.swift +++ b/RudderCore/Sources/Core/RudderProtocol.swift @@ -1,5 +1,5 @@ // -// RSClientProtocol.swift +// RudderProtocol.swift // Rudder // // Created by Pallab Maiti on 06/02/24. @@ -69,7 +69,7 @@ public protocol RudderProtocol: AnyObject { /// - 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?) + func track(_ eventName: String, properties: TrackProperties?, option: MessageOptionType?) /// Set current user's information /// @@ -77,7 +77,7 @@ public protocol RudderProtocol: AnyObject { /// - 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?) + func identify(_ userId: String, traits: IdentifyTraits?, option: MessageOptionType?) /// Track a screen with name, category. /// @@ -86,7 +86,7 @@ public protocol RudderProtocol: AnyObject { /// - 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?) + func screen(_ screenName: String, category: String?, properties: ScreenProperties?, option: MessageOptionType?) /// Associate an user to a company or organization. /// @@ -94,12 +94,12 @@ public protocol RudderProtocol: AnyObject { /// - 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?) + func group(_ groupId: String, traits: GroupTraits?, option: MessageOptionType?) /// 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?) + func alias(_ newId: String, option: MessageOptionType?) } diff --git a/RudderCore/Sources/Core/SourceConfigDownload/SourceConfigDownload.swift b/RudderCore/Sources/Core/SourceConfigDownload/SourceConfigDownload.swift index 6d4a75ae..3fe8e3ef 100644 --- a/RudderCore/Sources/Core/SourceConfigDownload/SourceConfigDownload.swift +++ b/RudderCore/Sources/Core/SourceConfigDownload/SourceConfigDownload.swift @@ -10,12 +10,12 @@ import Foundation class SourceConfigDownload { private var downloader: SourceConfigDownloadWorkerType - var sourceConfig: ((SourceConfig, NeedsDatabaseMigration) -> Void) = { _, _ in } + var sourceConfig: ((SourceConfig) -> Void) = { _ in } init(downloader: SourceConfigDownloadWorkerType) { self.downloader = downloader - self.downloader.sourceConfig = { sourceConfig, needsDatabaseMigration in - self.sourceConfig(sourceConfig, needsDatabaseMigration) + self.downloader.sourceConfig = { sourceConfig in + self.sourceConfig(sourceConfig) } } } diff --git a/RudderCore/Sources/Core/SourceConfigDownload/SourceConfigDownloadWorker.swift b/RudderCore/Sources/Core/SourceConfigDownload/SourceConfigDownloadWorker.swift index 5e8001fe..a3727f2f 100644 --- a/RudderCore/Sources/Core/SourceConfigDownload/SourceConfigDownloadWorker.swift +++ b/RudderCore/Sources/Core/SourceConfigDownload/SourceConfigDownloadWorker.swift @@ -9,14 +9,12 @@ import Foundation import RudderInternal -typealias NeedsDatabaseMigration = Bool - protocol SourceConfigDownloadWorkerType { - var sourceConfig: ((SourceConfig, NeedsDatabaseMigration) -> Void) { get set } + var sourceConfig: ((SourceConfig) -> Void) { get set } } class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { - var sourceConfig: ((SourceConfig, NeedsDatabaseMigration) -> Void) = { _, _ in } + var sourceConfig: ((SourceConfig) -> Void) = { _ in } let sourceConfigDownloader: SourceConfigDownloaderType let downloadBlockers: DownloadUploadBlockersProtocol @@ -51,7 +49,7 @@ class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { guard let self = self else { return } if let sourceConfig: SourceConfig = userDefaults.read(.sourceConfig) { self.cachedSourceConfig = sourceConfig - self.sourceConfig(sourceConfig, false) + self.sourceConfig(sourceConfig) } let blockersForDownload = downloadBlockers.get() if blockersForDownload.isEmpty { @@ -70,7 +68,7 @@ class SourceConfigDownloadWorker: SourceConfigDownloadWorkerType { if let sourceConfig = response.sourceConfig { self.retryStrategy.reset() self.logger.logDebug(.sourceConfigDownloadSuccess) - self.sourceConfig(sourceConfig, self.needsMigration(freshSourceConfig: sourceConfig)) + self.sourceConfig(sourceConfig) } let downloadStatus = response.status if downloadStatus.needsRetry { @@ -98,12 +96,3 @@ 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/RudderCore/Sources/Core/Storage/Migration/StorageMigration.swift b/RudderCore/Sources/Core/Storage/Migration/StorageMigration.swift index bf76d988..3fef5457 100644 --- a/RudderCore/Sources/Core/Storage/Migration/StorageMigration.swift +++ b/RudderCore/Sources/Core/Storage/Migration/StorageMigration.swift @@ -15,7 +15,7 @@ class StorageMigration { self.storageMigrator = storageMigrator } - func migrate() throws { - try storageMigrator.migrate() + func migrate() { + storageMigrator.migrate() } } diff --git a/RudderCore/Sources/Core/Storage/Migration/StorageMigrator.swift b/RudderCore/Sources/Core/Storage/Migration/StorageMigrator.swift index 7bd8372c..cb9a0e41 100644 --- a/RudderCore/Sources/Core/Storage/Migration/StorageMigrator.swift +++ b/RudderCore/Sources/Core/Storage/Migration/StorageMigrator.swift @@ -7,34 +7,84 @@ // import Foundation +import RudderInternal protocol StorageMigrator { var currentStorage: Storage { get set } - func migrate() throws + func migrate() } class StorageMigratorV1V2: StorageMigrator { - let oldSQLiteStorage: SQLiteStorage + + let legacyDatabaseName = "rl_persistence.sqlite" + lazy var legacyDatabasePath : String = { Device.current.directoryPath.appendingPathComponent(legacyDatabaseName).path + }() + + let logger: Logger var currentStorage: Storage + let currentSourceConfig: SourceConfig - init(oldSQLiteStorage: SQLiteStorage, currentStorage: Storage) { - self.oldSQLiteStorage = oldSQLiteStorage + + init(currentStorage: Storage, currentSourceConfig: SourceConfig, logger: Logger) { self.currentStorage = currentStorage + self.currentSourceConfig = currentSourceConfig + self.logger = logger + } + + func migrate() { + if isMigrationNeeded() { + let legacyDatabase = SQLiteDatabase(path: Device.current.directoryPath, name:legacyDatabaseName) + let legacyStorage = SQLiteStorage(database: legacyDatabase, logger: logger) + legacyStorage.open() + let result = legacyStorage.objects(limit: .max) + switch result { + case .success(let list): + list.forEach({ _ = currentStorage.save($0) }) + _ = legacyStorage.close() + deleteLegacyDatabase() + logger.logDebug(.storageMigrationSuccess) + case .failure(let error): + logger.logError(.storageMigrationFailed(.storageError(error.localizedDescription))) + } + } } - func migrate() throws { - let databasePath = oldSQLiteStorage.database.path.appendingPathComponent(oldSQLiteStorage.database.name).path - guard FileManager.default.fileExists(atPath: databasePath) else { - throw StorageError.databaseNotExists + func isMigrationNeeded() -> Bool { + guard doesLegacyDatabaseExists() else { + logger.logDebug(.legacyDatabaseDoesNotExists) + return false + } + guard let legacySourceConfig = getLegacySourceConfig() else { + logger.logError(.storageMigrationFailedToReadSourceConfig) + deleteLegacyDatabase() + return false + } + if legacySourceConfig.source?.id == currentSourceConfig.source?.id { + return true } - 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 + return false + } + + func doesLegacyDatabaseExists() -> Bool { + FileManager.default.fileExists(atPath: legacyDatabasePath) + } + + /// We are reading legacy SourceConfig from Standard Defaults as v1 iOS SDK uses StandardDefaults + /// SourceConfig saved by v1 iOS SDK to StandardDefaults needs to be decoded using JSONDecoder opposed to PropertyListDecoder by v2 SDK. + func getLegacySourceConfig() -> SourceConfig? { + let standardDefaultsWorker = UserDefaultsWorker(userDefaults: UserDefaults.standard, queue: DispatchQueue(label: "standardDefaults".queueLabel())) + let sourceConfigString: String? = standardDefaultsWorker.read(.legacySourceConfig) + if let sourceConfigString = sourceConfigString { + return try? JSONDecoder().decode(SourceConfig.self, from: Data(sourceConfigString.utf8)) + } + return nil + } + + func deleteLegacyDatabase() { + do { + try FileManager.default.removeItem(atPath: legacyDatabasePath) + } catch { + logger.logError(.failedToDeleteLegacyDatabase(error.localizedDescription)) } } } diff --git a/RudderCore/Sources/RSClient.swift b/RudderCore/Sources/RSClient.swift index c9651285..2fb311fe 100644 --- a/RudderCore/Sources/RSClient.swift +++ b/RudderCore/Sources/RSClient.swift @@ -240,7 +240,7 @@ extension RSClient { /// - 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) { + public func track(_ eventName: String, properties: TrackProperties? = nil, option: MessageOptionType? = nil) { core.track(eventName, properties: properties, option: option) } @@ -250,7 +250,7 @@ extension RSClient { /// - 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) { + public func identify(_ userId: String, traits: IdentifyTraits? = nil, option: MessageOptionType? = nil) { core.identify(userId, traits: traits, option: option) } @@ -261,7 +261,7 @@ extension RSClient { /// - 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) { + public func screen(_ screenName: String, category: String? = nil, properties: ScreenProperties? = nil, option: MessageOptionType? = nil) { core.screen(screenName, category: category, properties: properties, option: option) } @@ -271,7 +271,7 @@ extension RSClient { /// - 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) { + public func group(_ groupId: String, traits: GroupTraits? = nil, option: MessageOptionType? = nil) { core.group(groupId, traits: traits, option: option) } @@ -280,7 +280,7 @@ extension RSClient { /// - Parameters: /// - groupId: User's new ID. /// - option: Event level options, if any. - public func alias(_ newId: String, option: MessageOption? = nil) { + public func alias(_ newId: String, option: MessageOptionType? = nil) { core.alias(newId, option: option) } } @@ -347,8 +347,8 @@ extension RSClient { /// /// - Parameters: /// - option: Options related to every API call - public func setOption(_ option: Option) { - core.setOption(option) + public func setGlobalOption(_ globalOption: GlobalOptionType) { + core.setGlobalOption(globalOption) } /// API for setting device token for Push Notifications to the destinations. diff --git a/RudderCore/Tests/Core/ClientTests.swift b/RudderCore/Tests/Core/ClientTests.swift index c83f6e14..118fba51 100644 --- a/RudderCore/Tests/Core/ClientTests.swift +++ b/RudderCore/Tests/Core/ClientTests.swift @@ -32,7 +32,8 @@ class ClientTests: XCTestCase { controlPlaneURL: "https://www.rudder.controlplane.com", autoSessionTracking: false, sessionTimeOut: 5000, - gzipEnabled: false + gzipEnabled: false, + dataResidencyServer: .EU ) client = .mockWith( @@ -174,8 +175,8 @@ class ClientTests: XCTestCase { func testScreen_Option() { let option = MessageOption() - .putIntegration("Destination_1", isEnabled: true) - .putIntegration("Destination_2", isEnabled: false) + .putIntegrationStatus("Destination_1", isEnabled: true) + .putIntegrationStatus("Destination_2", isEnabled: false) .putCustomContext(["n_key_1": "n_value_1"], for: "key_1") client.screen("ViewController", option: option) @@ -201,8 +202,8 @@ class ClientTests: XCTestCase { func testScreen_Option_Properties() { let option = MessageOption() - .putIntegration("Destination_1", isEnabled: true) - .putIntegration("Destination_2", isEnabled: false) + .putIntegrationStatus("Destination_1", isEnabled: true) + .putIntegrationStatus("Destination_2", isEnabled: false) .putCustomContext(["n_key_1": "n_value_1"], for: "key_1") client.screen("ViewController", properties: ["key_3": "value_3", "key_4": "value_4"], option: option) @@ -258,8 +259,8 @@ class ClientTests: XCTestCase { func testScreen_Category_Option() { let option = MessageOption() - .putIntegration("Destination_1", isEnabled: true) - .putIntegration("Destination_2", isEnabled: false) + .putIntegrationStatus("Destination_1", isEnabled: true) + .putIntegrationStatus("Destination_2", isEnabled: false) .putCustomContext(["n_key_1": "n_value_1"], for: "key_1") client.screen("ViewController", category: "category_1", option: option) @@ -285,8 +286,8 @@ class ClientTests: XCTestCase { func testScreen_Category_Option_Properties() { let option = MessageOption() - .putIntegration("Destination_1", isEnabled: true) - .putIntegration("Destination_2", isEnabled: false) + .putIntegrationStatus("Destination_1", isEnabled: true) + .putIntegrationStatus("Destination_2", isEnabled: false) .putCustomContext(["n_key_1": "n_value_1"], for: "key_1") client.screen("ViewController", category: "category_1", properties: ["key_3": "value_3", "key_4": "value_4"], option: option) @@ -344,8 +345,8 @@ class ClientTests: XCTestCase { // Track with eventName and option func testTrack_Option() { let option = MessageOption() - .putIntegration("Destination_1", isEnabled: true) - .putIntegration("Destination_2", isEnabled: false) + .putIntegrationStatus("Destination_1", isEnabled: true) + .putIntegrationStatus("Destination_2", isEnabled: false) .putCustomContext(["n_key_1": "n_value_1"], for: "key_1") client.track("simple_track_with_option", option: option) @@ -371,8 +372,8 @@ class ClientTests: XCTestCase { // Track with eventName, properties and option func testTrack_Properties_Option() { let option = MessageOption() - .putIntegration("Destination_3", isEnabled: true) - .putIntegration("Destination_4", isEnabled: false) + .putIntegrationStatus("Destination_3", isEnabled: true) + .putIntegrationStatus("Destination_4", isEnabled: false) .putCustomContext(["n_key_2": "n_value_2"], for: "key_2") client.track("simple_track_with_props_and_option", properties: ["key_3": "value_3", "key_4": "value_4"], option: option) @@ -485,15 +486,16 @@ class ClientTests: XCTestCase { XCTAssertEqual(config.automaticSessionTracking, configuration.automaticSessionTracking) XCTAssertEqual(config.sessionTimeOut, configuration.sessionTimeOut) XCTAssertEqual(config.gzipEnabled, configuration.gzipEnabled) + XCTAssertEqual(config.dataResidencyServer, configuration.dataResidencyServer) } func testOption() throws { - let globalOption = Option() - .putIntegration("destination_1", isEnabled: true) - .putIntegration("destination_2", isEnabled: true) - .putIntegration("destination_3", isEnabled: false) - - client.setOption(globalOption) + let globalOption = MessageOption() + .putIntegrationStatus("destination_1", isEnabled: true) + .putIntegrationStatus("destination_2", isEnabled: true) + .putIntegrationStatus("destination_3", isEnabled: false) + + client.setGlobalOption(globalOption) client.track("test_track") diff --git a/RudderCore/Tests/Core/Networking/ServiceManagerTests.swift b/RudderCore/Tests/Core/Networking/ServiceManagerTests.swift index 7814983c..397891e9 100644 --- a/RudderCore/Tests/Core/Networking/ServiceManagerTests.swift +++ b/RudderCore/Tests/Core/Networking/ServiceManagerTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import Rudder +@testable import RudderInternal final class ServiceManagerTests: XCTestCase { diff --git a/RudderCore/Tests/Core/Plugins/EventFilteringTests.swift b/RudderCore/Tests/Core/Plugins/EventFilteringTests.swift index 182cd04d..3c9635fd 100644 --- a/RudderCore/Tests/Core/Plugins/EventFilteringTests.swift +++ b/RudderCore/Tests/Core/Plugins/EventFilteringTests.swift @@ -7,8 +7,8 @@ // import XCTest -import RudderInternal @testable import Rudder +@testable import RudderInternal final class EventFilteringTests: XCTestCase { func test_whiteListEvent() throws { @@ -128,7 +128,7 @@ class EventFilteringTestDestination: DestinationPlugin { var name: String = "test_destination" var plugins: [Rudder.Plugin] = [] var type: Rudder.PluginType = .destination - var client: Rudder.RudderProtocol? + var client: RudderProtocol? var sourceConfig: Rudder.SourceConfig? var lastMessage: TrackMessage? diff --git a/RudderCore/Tests/Core/Plugins/ReplayQueuePluginTests.swift b/RudderCore/Tests/Core/Plugins/ReplayQueuePluginTests.swift index 03b26a40..16800ed5 100644 --- a/RudderCore/Tests/Core/Plugins/ReplayQueuePluginTests.swift +++ b/RudderCore/Tests/Core/Plugins/ReplayQueuePluginTests.swift @@ -263,7 +263,7 @@ class ReplayQueueTestDestination: DestinationPlugin { var name: String = "test_destination" var plugins: [Rudder.Plugin] = [] var type: Rudder.PluginType = .destination - var client: Rudder.RudderProtocol? + var client: RudderProtocol? var sourceConfig: Rudder.SourceConfig? { didSet { onUpdateSourceConfig?() diff --git a/RudderCore/Tests/Core/Plugins/UserSessionPluginTests.swift b/RudderCore/Tests/Core/Plugins/UserSessionPluginTests.swift index 442169ef..56bfdfc2 100644 --- a/RudderCore/Tests/Core/Plugins/UserSessionPluginTests.swift +++ b/RudderCore/Tests/Core/Plugins/UserSessionPluginTests.swift @@ -113,7 +113,7 @@ final class UserSessionPluginTests: XCTestCase { XCTAssertFalse(trackMessage_2?.sessionStart ?? false) XCTAssertEqual(trackMessage?.sessionId, trackMessage_2?.sessionId) - sleep(bySeconds: 2) + usleep(2000000) // When userSessionPlugin.startSession() @@ -233,7 +233,7 @@ final class UserSessionPluginTests: XCTestCase { XCTAssertNotNil(trackMessage?.sessionId) XCTAssertTrue(trackMessage?.sessionStart ?? false) - sleep(bySeconds: 2) + usleep(2000000) // When userSessionPlugin.refreshSessionIfNeeded() @@ -264,7 +264,7 @@ final class UserSessionPluginTests: XCTestCase { XCTAssertTrue(trackMessage?.sessionStart ?? false) // When - sleep(bySeconds: 2) + usleep(2000000) userSessionPlugin.reset() let trackMessage_2 = userSessionPlugin.process(message: TrackMessage(event: "test_session_2")) @@ -297,7 +297,7 @@ final class UserSessionPluginTests: XCTestCase { XCTAssertNotNil(trackMessage_2?.sessionId) XCTAssertTrue(trackMessage_2?.sessionStart ?? false) - sleep(bySeconds: 2) + usleep(2000000) // When userSessionPlugin.reset() diff --git a/RudderCore/Tests/Core/SourceConfigDownload/SourceConfigDownloadTests.swift b/RudderCore/Tests/Core/SourceConfigDownload/SourceConfigDownloadTests.swift index 50cb55df..f9ffbd45 100644 --- a/RudderCore/Tests/Core/SourceConfigDownload/SourceConfigDownloadTests.swift +++ b/RudderCore/Tests/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/RudderCore/Tests/Core/SourceConfigDownload/SourceConfigDownloadWorkerTests.swift b/RudderCore/Tests/Core/SourceConfigDownload/SourceConfigDownloadWorkerTests.swift index ad9a920b..c57b711f 100644 --- a/RudderCore/Tests/Core/SourceConfigDownload/SourceConfigDownloadWorkerTests.swift +++ b/RudderCore/Tests/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/RudderCore/Tests/Core/Storage/StorageMigratorTests.swift b/RudderCore/Tests/Core/Storage/StorageMigratorTests.swift index b169e0f7..29d9722a 100644 --- a/RudderCore/Tests/Core/Storage/StorageMigratorTests.swift +++ b/RudderCore/Tests/Core/Storage/StorageMigratorTests.swift @@ -8,53 +8,112 @@ import XCTest @testable import Rudder +@testable import RudderInternal final class StorageMigratorTests: XCTestCase { - func test_migrate() throws { + let legacyStorageName = "rl_persistence.sqlite" + let currentStorageName = "rl_persistence_default_test.sqlite" + + func test_migration_when_sourceId_matches() { // 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()) - ) + /// creating legacy storage, and then opening it and deleting if there are any events already in the storage + let legacyStorage = createStorage(storageName: legacyStorageName) + legacyStorage.open() + legacyStorage.deleteAll() + /// adding some dummy events to the legacy storage to test the migration + addDummyEventsToStorage(storage: legacyStorage, count: 5) + XCTAssertEqual(try legacyStorage.count().get(), 5) + legacyStorage.close() - oldSQLiteStorage.open() - oldSQLiteStorage.deleteAll() + /// creating current storage, and then opening it and deleting if there are any events already in the storage + let currentStorage = createStorage(storageName: currentStorageName) + currentStorage.open() + currentStorage.deleteAll() + /// adding some dummy events to the current storage to test the migration + addDummyEventsToStorage(storage: currentStorage, count: 2) + XCTAssertEqual(try currentStorage.count().get(), 2) - let databasePath = path.appendingPathComponent("rl_persistence_test.sqlite").path - XCTAssertTrue(FileManager.default.fileExists(atPath: databasePath)) + let currentSourceConfig = getSourceConfig(sourceId: "source1") + /// Setting the legacy SourceConfig to same value as the current SourceConfig so that the migrator performs migration + UserDefaults.standard.setValue(String(decoding: try! JSONEncoder().encode(currentSourceConfig), as: UTF8.self), forKey: UserDefaultsKeys.legacySourceConfig.rawValue) - 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() + // When + let storageMigrator = StorageMigratorV1V2(currentStorage: currentStorage, currentSourceConfig: currentSourceConfig, logger: Logger(logger: NOLogger())) + storageMigrator.migrate() + + // Then + XCTAssertEqual(try currentStorage.count().get(), 7) + XCTAssertFalse(FileManager.default.fileExists(atPath: getStoragePath(name: legacyStorageName))) - currentStorage.save(StorageMessage(id: "", message: "message_1", updated: 1246573777)) - currentStorage.save(StorageMessage(id: "", message: "message_2", updated: 1223546723)) + currentStorage.close() + deleteStorage(name: legacyStorageName) + deleteStorage(name: currentStorageName) + } + + func test_migration_when_sourceIds_are_different() { + // Given + /// creating legacy storage, and then opening it and deleting if there are any events already in the storage + let legacyStorage = createStorage(storageName: legacyStorageName) + legacyStorage.open() + legacyStorage.deleteAll() + /// adding some dummy events to the legacy storage to test the migration + addDummyEventsToStorage(storage: legacyStorage, count: 5) + XCTAssertEqual(try legacyStorage.count().get(), 5) + legacyStorage.close() + /// creating current storage, and then opening it and deleting if there are any events already in the storage + let currentStorage = createStorage(storageName: currentStorageName) + currentStorage.open() + currentStorage.deleteAll() + /// adding some dummy events to the current storage to test the migration + addDummyEventsToStorage(storage: currentStorage, count: 2) XCTAssertEqual(try currentStorage.count().get(), 2) - let storageMigrator = StorageMigratorV1V2(oldSQLiteStorage: oldSQLiteStorage, currentStorage: currentStorage) + let currentSourceConfig = getSourceConfig(sourceId: "source1") + let legacySourceConfig = getSourceConfig(sourceId: "source2") + /// Setting the legacy SourceConfig to a value different from current SourceConfig so that the migration will not happen + UserDefaults.standard.setValue(String(decoding: try! JSONEncoder().encode(legacySourceConfig), as: UTF8.self), forKey: UserDefaultsKeys.legacySourceConfig.rawValue) + // When - try storageMigrator.migrate() + let storageMigrator = StorageMigratorV1V2(currentStorage: currentStorage, currentSourceConfig: currentSourceConfig, logger: Logger(logger: NOLogger())) + storageMigrator.migrate() // Then - XCTAssertEqual(try currentStorage.count().get(), 6) - XCTAssertFalse(FileManager.default.fileExists(atPath: databasePath)) + XCTAssertEqual(try currentStorage.count().get(), 2) + XCTAssertTrue(FileManager.default.fileExists(atPath: getStoragePath(name: legacyStorageName))) - oldSQLiteStorage.close() currentStorage.close() + deleteStorage(name: legacyStorageName) + deleteStorage(name: currentStorageName) + } + + func createStorage(storageName: String) -> SQLiteStorage { + let database = SQLiteDatabase(path: Device.current.directoryPath, name: storageName) + let storage = SQLiteStorage( + database: database, + logger: Logger(logger: NOLogger()) + ) + return storage + } + + func addDummyEventsToStorage(storage: SQLiteStorage, count: Int) { + for i in 1...count { + storage.save(StorageMessage(id: "", message: "message_\(storage.database.name)_\(i)", updated: .getTimeStamp())) + } + } + + func getStoragePath(name: String) -> String { + Device.current.directoryPath.appendingPathComponent(name).path + } + + func deleteStorage(name: String) { + try? FileManager.default.removeItem(atPath: getStoragePath(name: name)) + } + + func getSourceConfig(sourceId: String) -> SourceConfig { + SourceConfig(source: SourceConfig.Source(id: sourceId, name: nil, writeKey: nil, enabled: nil, sourceDefinitionId: nil, createdBy: nil, workspaceId: nil, deleted: nil, createdAt: nil, updatedAt: nil, destinations: nil, dataPlanes: nil)) } } diff --git a/RudderCore/Tests/Mocks/CoreMocks.swift b/RudderCore/Tests/Mocks/CoreMocks.swift index 157b20ac..96c517de 100644 --- a/RudderCore/Tests/Mocks/CoreMocks.swift +++ b/RudderCore/Tests/Mocks/CoreMocks.swift @@ -30,7 +30,8 @@ extension Configuration { flushPolicies: [FlushPolicy] = [FlushPolicy](), dataUploadRetryPolicy: RetryPolicy? = nil, sourceConfigDownloadRetryPolicy: RetryPolicy? = nil, - logger: LoggerProtocol? = NOLogger() + logger: LoggerProtocol? = NOLogger(), + dataResidencyServer: DataResidencyServer = Constants.residencyServer.default ) -> Configuration { .init(writeKey: writeKey, dataPlaneURL: dataPlaneURL)! .flushQueueSize(flushQueueSize) @@ -46,6 +47,7 @@ extension Configuration { .dataUploadRetryPolicy(dataUploadRetryPolicy) .sourceConfigDownloadRetryPolicy(sourceConfigDownloadRetryPolicy) .logger(logger) + .dataResidencyServer(dataResidencyServer) } } @@ -349,7 +351,7 @@ extension RSClient { class StorageMigratorMock: StorageMigrator { var currentStorage: Storage = StorageMock() - func migrate() throws { + func migrate() { } } diff --git a/RudderCore/Tests/Mocks/RSClientMock.swift b/RudderCore/Tests/Mocks/RSClientMock.swift index 7b82ce8a..d6017966 100644 --- a/RudderCore/Tests/Mocks/RSClientMock.swift +++ b/RudderCore/Tests/Mocks/RSClientMock.swift @@ -11,27 +11,27 @@ import RudderInternal @testable import Rudder class RSClientMock: RudderProtocol { - func track(_ eventName: String, properties: Rudder.TrackProperties?, option: Rudder.MessageOption?) { + func track(_ eventName: String, properties: Rudder.TrackProperties?, option: MessageOptionType?) { let message = TrackMessage(event: eventName, properties: properties, option: option) process(message: message) } - func identify(_ userId: String, traits: Rudder.IdentifyTraits?, option: Rudder.IdentifyOptionType?) { + func identify(_ userId: String, traits: Rudder.IdentifyTraits?, option: MessageOptionType?) { let message = IdentifyMessage(userId: userId, traits: traits, option: option) process(message: message) } - func screen(_ screenName: String, category: String?, properties: Rudder.ScreenProperties?, option: Rudder.MessageOption?) { + func screen(_ screenName: String, category: String?, properties: Rudder.ScreenProperties?, option: MessageOptionType?) { let message = ScreenMessage(title: screenName, category: category, properties: properties, option: option) process(message: message) } - func group(_ groupId: String, traits: Rudder.GroupTraits?, option: Rudder.MessageOption?) { + func group(_ groupId: String, traits: Rudder.GroupTraits?, option: MessageOptionType?) { let message = GroupMessage(groupId: groupId, traits: traits, option: option) process(message: message) } - func alias(_ newId: String, option: Rudder.MessageOption?) { + func alias(_ newId: String, option: MessageOptionType?) { let message = AliasMessage(newId: newId, option: option) process(message: message) } diff --git a/RudderTests/Core/DataResidencyTests.swift b/RudderTests/Core/DataResidencyTests.swift new file mode 100644 index 00000000..12ac7dd0 --- /dev/null +++ b/RudderTests/Core/DataResidencyTests.swift @@ -0,0 +1,397 @@ +// +// DataResidencyTests.swift +// Rudder +// +// Created by Pallab Maiti on 09/01/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import XCTest +@testable import Rudder + +let EU = "https://rudderstacgwyx-eu.dataplane.rudderstack.com" +let US = "https://rudderstacgwyx-us.dataplane.rudderstack.com" + +final class DataResidencyTests: XCTestCase { + + func testWithBothResidenciesInSourceConfig_1() { + let sourceConfig: SourceConfig = MultiDataResidency.defaultTrue + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == US) + } + + func testWithBothResidenciesInSourceConfig_2() { + let sourceConfig: SourceConfig = MultiDataResidency.defaultTrue + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == EU) + } + + func testWithBothResidenciesInSourceConfig_3() { + let sourceConfig: SourceConfig = MultiDataResidency.defaultTrue + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == US) + } + + func testWithBothResidenciesInSourceConfig_DefaultFalse_1() { + let sourceConfig: SourceConfig = MultiDataResidency.defaultFalse + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithBothResidenciesInSourceConfig_DefaultFalse_2() { + let sourceConfig: SourceConfig = MultiDataResidency.defaultFalse + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithBothResidenciesInSourceConfig_DefaultFalse_3() { + let sourceConfig: SourceConfig = MultiDataResidency.defaultFalse + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithBothResidenciesInSourceConfig_USTrue_1() { + let sourceConfig: SourceConfig = MultiDataResidency.USTrue + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == US) + } + + func testWithBothResidenciesInSourceConfig_USTrue_2() { + let sourceConfig: SourceConfig = MultiDataResidency.USTrue + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithBothResidenciesInSourceConfig_USTrue_3() { + let sourceConfig: SourceConfig = MultiDataResidency.USTrue + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == US) + } + + func testWithBothResidenciesInSourceConfig_EUTrue_1() { + let sourceConfig: SourceConfig = MultiDataResidency.EUTrue + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithBothResidenciesInSourceConfig_EUTrue_2() { + let sourceConfig: SourceConfig = MultiDataResidency.EUTrue + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == EU) + } + + func testWithBothResidenciesInSourceConfig_EUTrue_3() { + let sourceConfig: SourceConfig = MultiDataResidency.EUTrue + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithOnlyUSInSourceConfig_1() { + let sourceConfig: SourceConfig = USDataResidency.defaultTrue + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == US) + } + + func testWithOnlyUSInSourceConfig_2() { + let sourceConfig: SourceConfig = USDataResidency.defaultTrue + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == US) + } + + func testWithOnlyUSInSourceConfig_3() { + let sourceConfig: SourceConfig = USDataResidency.defaultTrue + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == US) + } + + func testWithOnlyUSInSourceConfig_DefaultFalse_1() { + let sourceConfig: SourceConfig = USDataResidency.defaultFalse + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithOnlyUSInSourceConfig_DefaultFalse_2() { + let sourceConfig: SourceConfig = USDataResidency.defaultFalse + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithOnlyUSInSourceConfig_DefaultFalse_3() { + let sourceConfig: SourceConfig = USDataResidency.defaultFalse + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithOnlyEUInSourceConfig_1() { + let sourceConfig: SourceConfig = EUDataResidency.defaultTrue + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithOnlyEUInSourceConfig_2() { + let sourceConfig: SourceConfig = EUDataResidency.defaultTrue + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNotNil(dataResidency.dataPlaneUrl) + XCTAssertTrue(dataResidency.dataPlaneUrl == EU) + } + + func testWithOnlyEUInSourceConfig_3() { + let sourceConfig: SourceConfig = EUDataResidency.defaultTrue + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithOnlyEUInSourceConfig_DefaultFalse_1() { + let sourceConfig: SourceConfig = EUDataResidency.defaultFalse + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithOnlyEUInSourceConfig_DefaultFalse_2() { + let sourceConfig: SourceConfig = EUDataResidency.defaultFalse + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWithOnlyEUInSourceConfig_DefaultFalse_3() { + let sourceConfig: SourceConfig = EUDataResidency.defaultFalse + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWhenNoUrlInSourceConfig_1() { + let sourceConfig: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: nil + ) + ) + let config: Configuration = .mockWith() + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWhenNoUrlInSourceConfig_2() { + let sourceConfig: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: nil + ) + ) + let config: Configuration = .mockWith(dataResidencyServer: .EU) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWhenNoUrlInSourceConfig_3() { + let sourceConfig: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: nil + ) + ) + let config: Configuration = .mockWith(dataResidencyServer: .US) + let dataResidency = DataResidency(dataResidencyServer: config.dataResidencyServer, sourceConfig: sourceConfig) + + XCTAssertNil(dataResidency.dataPlaneUrl) + } + + func testWhenNoUrlInSourceConfig_4() { + let config: Configuration? = Configuration(writeKey: WRITE_KEY, dataPlaneURL: "https::/dataplanerudderstackcom") + XCTAssertNil(config) + } +} + +class MultiDataResidency { + static let defaultTrue: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: .mockWith( + eu: [ + .mockWith( + url: EU, + default: true + ) + ], + us: [ + .mockWith( + url: US, + default: true + ) + ] + ) + ) + ) + + static let defaultFalse: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: .mockWith( + eu: [ + .mockWith( + url: EU, + default: false + ) + ], + us: [ + .mockWith( + url: US, + default: false + ) + ] + ) + ) + ) + + static let USTrue: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: .mockWith( + eu: [ + .mockWith( + url: EU, + default: false + ) + ], + us: [ + .mockWith( + url: US, + default: true + ) + ] + ) + ) + ) + + static let EUTrue: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: .mockWith( + eu: [ + .mockWith( + url: EU, + default: true + ) + ], + us: [ + .mockWith( + url: US, + default: false + ) + ] + ) + ) + ) +} + +class USDataResidency { + static let defaultTrue: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: .mockWith( + eu: nil, + us: [ + .mockWith( + url: US, + default: true + ) + ] + ) + ) + ) + + static let defaultFalse: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: .mockWith( + eu: nil, + us: [ + .mockWith( + url: US, + default: false + ) + ] + ) + ) + ) +} + +class EUDataResidency { + static let defaultTrue: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: .mockWith( + eu: [ + .mockWith( + url: EU, + default: true + ) + ], + us: nil + ) + ) + ) + + static let defaultFalse: SourceConfig = .mockWith( + source: .mockWith( + dataPlanes: .mockWith( + eu: [ + .mockWith( + url: EU, + default: false + ) + ], + us: nil + ) + ) + ) +} diff --git a/RudderTests/Mocks/NOLogger.swift b/RudderTests/Mocks/NOLogger.swift new file mode 100644 index 00000000..65c3254a --- /dev/null +++ b/RudderTests/Mocks/NOLogger.swift @@ -0,0 +1,16 @@ +// +// NOLogger.swift +// RudderStackTests +// +// Created by Pallab Maiti on 22/01/24. +// Copyright © 2024 Rudder Labs India Pvt Ltd. All rights reserved. +// + +import Foundation +@testable import Rudder + +class NOLogger: LoggerProtocol { + func log(_ message: String, logLevel: Rudder.LogLevel, file: String, function: String, line: Int) { + + } +}