diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 0a4c2823..a2304653 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -130,11 +130,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @unknown default: print("Optimizely SDK initiliazation failed with unknown result") } + self.startWithRootViewController() // For sample codes for APIs, see "Samples/SamplesForAPI.swift" //SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely) //SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely) + //SamplesForAPI.checkAudienceSegments(optimizely: self.optimizely) } } diff --git a/DemoSwiftApp/DemoSwiftApp.xcodeproj/project.pbxproj b/DemoSwiftApp/DemoSwiftApp.xcodeproj/project.pbxproj index 03a35575..89ad89ed 100644 --- a/DemoSwiftApp/DemoSwiftApp.xcodeproj/project.pbxproj +++ b/DemoSwiftApp/DemoSwiftApp.xcodeproj/project.pbxproj @@ -555,7 +555,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1230; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1320; ORGANIZATIONNAME = Optimizely; TargetAttributes = { 252D7DEC21C8800800134A7A = { diff --git a/DemoSwiftApp/Samples/SamplesForAPI.swift b/DemoSwiftApp/Samples/SamplesForAPI.swift index 02be33a1..39179edd 100644 --- a/DemoSwiftApp/Samples/SamplesForAPI.swift +++ b/DemoSwiftApp/Samples/SamplesForAPI.swift @@ -16,6 +16,7 @@ import Foundation import Optimizely +import UIKit class SamplesForAPI { @@ -146,17 +147,17 @@ class SamplesForAPI { // (1) set a forced decision for a flag - var success = user.setForcedDecision(context: context1, decision: forced1) + _ = user.setForcedDecision(context: context1, decision: forced1) decision = user.decide(key: "flag-1") // (2) set a forced decision for an ab-test rule - success = user.setForcedDecision(context: context2, decision: forced2) + _ = user.setForcedDecision(context: context2, decision: forced2) decision = user.decide(key: "flag-1") // (3) set a forced variation for a delivery rule - success = user.setForcedDecision(context: context3, + _ = user.setForcedDecision(context: context3, decision: forced3) decision = user.decide(key: "flag-1") @@ -167,8 +168,8 @@ class SamplesForAPI { // (5) remove forced variations - success = user.removeForcedDecision(context: context2) - success = user.removeAllForcedDecisions() + _ = user.removeForcedDecision(context: context2) + _ = user.removeAllForcedDecisions() } // MARK: - OptimizelyConfig @@ -260,6 +261,32 @@ class SamplesForAPI { } + // MARK: - AudienceSegments + + static func checkAudienceSegments(optimizely: OptimizelyClient) { + // override the default handler if cache size and timeout need to be customized + let optimizely = OptimizelyClient(sdkKey: "VivZyCGPHY369D4z8T9yG", // odp-test + periodicDownloadInterval: 60, + defaultLogLevel: .debug) + optimizely.start { result in + if case .failure(let error) = result { + print("[AudienceSegments] SDK initialization failed: \(error)") + return + } + + let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"]) + user.fetchQualifiedSegments(options: [.ignoreCache]) { _, error in + guard error == nil else { + print("[AudienceSegments] \(error!.errorDescription!)") + return + } + + let decision = user.decide(key: "show_coupon", options: [.includeReasons]) + print("[AudienceSegments] decision: \(decision)") + } + } + } + // MARK: - Initializations static func samplesForInitialization() { diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index ceb2e999..f674aa24 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -625,6 +625,38 @@ 6E636BA02236C96700AF3CEF /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */; }; 6E6419DA2657059700C49555 /* NotificationCenterTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6419D92657059700C49555 /* NotificationCenterTests_MultiClients.swift */; }; 6E6419FE265734C100C49555 /* ProjectConfigTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6419FD265734C100C49555 /* ProjectConfigTests_MultiClients.swift */; }; + 6E6522DE278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522DF278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E0278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E1278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E2278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E3278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E4278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E5278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E6278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E7278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E8278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522E9278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522EA278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522EB278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522EC278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E6522ED278E4F3800954EA1 /* OdpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* OdpManager.swift */; }; + 6E652300278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652301278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652302278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652303278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652304278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652305278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652306278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652307278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652308278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E652309278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E65230A278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E65230B278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E65230C278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E65230D278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E65230E278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; + 6E65230F278E688B00954EA1 /* LruCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* LruCache.swift */; }; 6E6BE00B237F547200FE8274 /* optimizely_config_datafile.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E6BE009237F547200FE8274 /* optimizely_config_datafile.json */; }; 6E6BE00C237F547200FE8274 /* optimizely_config_expected.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E6BE00A237F547200FE8274 /* optimizely_config_expected.json */; }; 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75165F22C520D400B2B157 /* DefaultLogger.swift */; }; @@ -1364,7 +1396,6 @@ 6E9B114522C5486E00C22D81 /* MurmurTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75197F22C5211100B2B157 /* MurmurTests.swift */; }; 6E9B114622C5486E00C22D81 /* DecisionServiceTests_Experiments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */; }; 6E9B114722C5486E00C22D81 /* OptimizelyErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */; }; - 6E9B114822C5486E00C22D81 /* OptimizelySwiftSDKiOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198222C5211100B2B157 /* OptimizelySwiftSDKiOSTests.swift */; }; 6E9B114922C5486E00C22D81 /* BucketTests_GroupToExp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198322C5211100B2B157 /* BucketTests_GroupToExp.swift */; }; 6E9B114A22C5486E00C22D81 /* BucketTests_Others.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198422C5211100B2B157 /* BucketTests_Others.swift */; }; 6E9B114B22C5486E00C22D81 /* BatchEventBuilderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198522C5211100B2B157 /* BatchEventBuilderTest.swift */; }; @@ -1388,7 +1419,6 @@ 6E9B115F22C5487100C22D81 /* MurmurTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75197F22C5211100B2B157 /* MurmurTests.swift */; }; 6E9B116022C5487100C22D81 /* DecisionServiceTests_Experiments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */; }; 6E9B116122C5487100C22D81 /* OptimizelyErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */; }; - 6E9B116222C5487100C22D81 /* OptimizelySwiftSDKiOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198222C5211100B2B157 /* OptimizelySwiftSDKiOSTests.swift */; }; 6E9B116322C5487100C22D81 /* BucketTests_GroupToExp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198322C5211100B2B157 /* BucketTests_GroupToExp.swift */; }; 6E9B116422C5487100C22D81 /* BucketTests_Others.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198422C5211100B2B157 /* BucketTests_Others.swift */; }; 6E9B116522C5487100C22D81 /* BatchEventBuilderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75198522C5211100B2B157 /* BatchEventBuilderTest.swift */; }; @@ -1723,6 +1753,211 @@ 75C71A4625E454460084187E /* SDKVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167322C520D400B2B157 /* SDKVersion.swift */; }; 75C71C3925E45A2B0084187E /* WatchBackgroundNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C71C3825E45A2B0084187E /* WatchBackgroundNotifier.swift */; }; 7B4A4C11E9A503E68F2FCC69 /* libPods-OptimizelyTests-Common-iOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 33BE1D8564FE425132F728C0 /* libPods-OptimizelyTests-Common-iOS.a */; }; + 8428D3D02807337400D0FB0C /* LruCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3CF2807337400D0FB0C /* LruCacheTests.swift */; }; + 8428D3D12807337400D0FB0C /* LruCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3CF2807337400D0FB0C /* LruCacheTests.swift */; }; + 84518B1F287665020023F104 /* OptimizelyClientTests_ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B1E287665020023F104 /* OptimizelyClientTests_ODP.swift */; }; + 84518B21287737070023F104 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945BC2877589D00D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945BD2877589E00D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945BE2877589E00D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945BF2877589F00D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C02877589F00D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C1287758A000D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C2287758A000D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C3287758A100D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C4287758A100D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C5287758A200D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C6287758A300D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C7287758A300D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C8287758A500D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945C9287758A600D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 845945CA287758A700D13E11 /* OdpConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84518B20287737070023F104 /* OdpConfig.swift */; }; + 8464087028130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087128130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087228130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087328130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087428130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087528130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087628130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087728130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087828130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087928130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087A28130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087B28130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087C28130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087D28130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087E28130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 8464087F28130D3200CCF97D /* Integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464086F28130D3200CCF97D /* Integration.swift */; }; + 84640881281320F000CCF97D /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84640880281320F000CCF97D /* IntegrationTests.swift */; }; + 84640882281320F000CCF97D /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84640880281320F000CCF97D /* IntegrationTests.swift */; }; + 846414A2289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414A3289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414A4289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414A5289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414A6289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414A7289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414A8289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414A9289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414AA289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414AB289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414AC289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414AD289B2D0700C45EE2 /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */; }; + 846414AE289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414AF289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B0289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B1289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B2289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B3289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B4289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B5289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B6289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B7289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B8289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 846414B9289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */; }; + 84644AB328F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84644AB228F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift */; }; + 84644AB428F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84644AB228F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift */; }; + 848617C82863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617C92863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617CA2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617CB2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617CC2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617CD2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617CE2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617CF2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617D02863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617D12863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617D22863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617D32863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617D42863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617D52863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617D62863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617D72863DC2700B7F41B /* OdpSegmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */; }; + 848617DA2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617DB2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617DC2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617DD2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617DE2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617DF2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E02863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E12863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E22863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E32863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E42863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E52863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E62863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E72863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E82863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617E92863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */; }; + 848617EA2863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617EB2863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617EC2863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617ED2863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617EE2863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617EF2863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F02863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F12863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F22863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F32863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F42863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F52863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F62863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F72863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F82863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617F92863E21200B7F41B /* OdpEventApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* OdpEventApiManager.swift */; }; + 848617FB286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 848617FC286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 848617FD286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 848617FE286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 848617FF286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861800286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861801286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861802286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861803286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861804286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861805286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861806286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861807286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861808286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861809286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 8486180A286CF33700B7F41B /* OdpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617FA286CF33700B7F41B /* OdpEvent.swift */; }; + 84861811286D0B8900B7F41B /* OdpSegmentManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486180D286D0B8900B7F41B /* OdpSegmentManagerTests.swift */; }; + 84861812286D0B8900B7F41B /* OdpSegmentManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486180D286D0B8900B7F41B /* OdpSegmentManagerTests.swift */; }; + 84861813286D0B8900B7F41B /* OdpManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486180E286D0B8900B7F41B /* OdpManagerTests.swift */; }; + 84861814286D0B8900B7F41B /* OdpManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486180E286D0B8900B7F41B /* OdpManagerTests.swift */; }; + 84861815286D0B8900B7F41B /* OdpVuidManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486180F286D0B8900B7F41B /* OdpVuidManagerTests.swift */; }; + 84861816286D0B8900B7F41B /* OdpVuidManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486180F286D0B8900B7F41B /* OdpVuidManagerTests.swift */; }; + 84861817286D0B8900B7F41B /* OdpEventManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861810286D0B8900B7F41B /* OdpEventManagerTests.swift */; }; + 84861818286D0B8900B7F41B /* OdpEventManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861810286D0B8900B7F41B /* OdpEventManagerTests.swift */; }; + 8486181B286D188B00B7F41B /* OdpSegmentApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861819286D188B00B7F41B /* OdpSegmentApiManagerTests.swift */; }; + 8486181C286D188B00B7F41B /* OdpSegmentApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861819286D188B00B7F41B /* OdpSegmentApiManagerTests.swift */; }; + 8486181D286D188B00B7F41B /* OdpEventApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486181A286D188B00B7F41B /* OdpEventApiManagerTests.swift */; }; + 8486181E286D188B00B7F41B /* OdpEventApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486181A286D188B00B7F41B /* OdpEventApiManagerTests.swift */; }; + 84958C5E280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84958C5D280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift */; }; + 84958C5F280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84958C5D280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift */; }; + 84B4D75027E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75127E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75227E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75327E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75427E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75527E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75627E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75727E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75827E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75927E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75A27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75B27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75C27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75D27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75E27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84B4D75F27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */; }; + 84E2E9422852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9432852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9442852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9452852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9462852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9472852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9482852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9492852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E94A2852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E94B2852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E94C2852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E94D2852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E94E2852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E94F2852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9502852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E9512852A378001114AB /* OdpVuidManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* OdpVuidManager.swift */; }; + 84E2E96128540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96228540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96328540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96428540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96528540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96628540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96728540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96828540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96928540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96A28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96B28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96C28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96D28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96E28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E96F28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E97028540B5E001114AB /* OptimizelySdkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */; }; + 84E2E9722855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9732855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9742855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9752855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9762855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9772855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9782855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9792855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E97A2855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E97B2855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E97C2855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E97D2855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E97E2855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E97F2855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9802855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; + 84E2E9812855875E001114AB /* OdpEventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9712855875E001114AB /* OdpEventManager.swift */; }; 84E7ABBB27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */; }; 84E7ABBC27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */; }; 84E7ABBD27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */; }; @@ -1739,6 +1974,10 @@ 84E7ABC827D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */; }; 84E7ABC927D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */; }; 84E7ABCA27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */; }; + 84F6BAB327FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */; }; + 84F6BAB427FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */; }; + 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */; }; + 84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */; }; BD1C3E8524E4399C0084B4DA /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B97DD93249D327F003DE606 /* SemanticVersion.swift */; }; BD64853C2491474500F30986 /* Optimizely.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E75167A22C520D400B2B157 /* Optimizely.h */; settings = {ATTRIBUTES = (Public, ); }; }; BD64853E2491474500F30986 /* Audience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169822C520D400B2B157 /* Audience.swift */; }; @@ -1961,6 +2200,8 @@ 6E636B9B2236C96700AF3CEF /* OptimizelyTests-Legacy-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-Legacy-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6E6419D92657059700C49555 /* NotificationCenterTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterTests_MultiClients.swift; sourceTree = ""; }; 6E6419FD265734C100C49555 /* ProjectConfigTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectConfigTests_MultiClients.swift; sourceTree = ""; }; + 6E6522DD278E4F3800954EA1 /* OdpManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OdpManager.swift; sourceTree = ""; }; + 6E6522FF278E688B00954EA1 /* LruCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LruCache.swift; sourceTree = ""; }; 6E6BE009237F547200FE8274 /* optimizely_config_datafile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = optimizely_config_datafile.json; sourceTree = ""; }; 6E6BE00A237F547200FE8274 /* optimizely_config_expected.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = optimizely_config_expected.json; sourceTree = ""; }; 6E75165F22C520D400B2B157 /* DefaultLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLogger.swift; sourceTree = ""; }; @@ -2052,7 +2293,6 @@ 6E75197F22C5211100B2B157 /* MurmurTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MurmurTests.swift; sourceTree = ""; }; 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_Experiments.swift; sourceTree = ""; }; 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyErrorTests.swift; sourceTree = ""; }; - 6E75198222C5211100B2B157 /* OptimizelySwiftSDKiOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelySwiftSDKiOSTests.swift; sourceTree = ""; }; 6E75198322C5211100B2B157 /* BucketTests_GroupToExp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests_GroupToExp.swift; sourceTree = ""; }; 6E75198422C5211100B2B157 /* BucketTests_Others.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests_Others.swift; sourceTree = ""; }; 6E75198522C5211100B2B157 /* BatchEventBuilderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchEventBuilderTest.swift; sourceTree = ""; }; @@ -2148,7 +2388,32 @@ 6EF8DE3024BF7D69008B9488 /* DecisionReasons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecisionReasons.swift; sourceTree = ""; }; 75C719BB25E4519B0084187E /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 75C71C3825E45A2B0084187E /* WatchBackgroundNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchBackgroundNotifier.swift; sourceTree = ""; }; + 8428D3CF2807337400D0FB0C /* LruCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LruCacheTests.swift; sourceTree = ""; }; + 84518B1E287665020023F104 /* OptimizelyClientTests_ODP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_ODP.swift; sourceTree = ""; }; + 84518B20287737070023F104 /* OdpConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpConfig.swift; sourceTree = ""; }; + 8464086F28130D3200CCF97D /* Integration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Integration.swift; sourceTree = ""; }; + 84640880281320F000CCF97D /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; + 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_audience_segments.json; sourceTree = ""; }; + 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = odp_integrated_no_segments.json; sourceTree = ""; }; + 84644AB228F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP_2.swift; sourceTree = ""; }; + 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpSegmentManager.swift; sourceTree = ""; }; + 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpSegmentApiManager.swift; sourceTree = ""; }; + 848617D92863E21200B7F41B /* OdpEventApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpEventApiManager.swift; sourceTree = ""; }; + 848617FA286CF33700B7F41B /* OdpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpEvent.swift; sourceTree = ""; }; + 8486180D286D0B8900B7F41B /* OdpSegmentManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpSegmentManagerTests.swift; sourceTree = ""; }; + 8486180E286D0B8900B7F41B /* OdpManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpManagerTests.swift; sourceTree = ""; }; + 8486180F286D0B8900B7F41B /* OdpVuidManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpVuidManagerTests.swift; sourceTree = ""; }; + 84861810286D0B8900B7F41B /* OdpEventManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpEventManagerTests.swift; sourceTree = ""; }; + 84861819286D188B00B7F41B /* OdpSegmentApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpSegmentApiManagerTests.swift; sourceTree = ""; }; + 8486181A286D188B00B7F41B /* OdpEventApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpEventApiManagerTests.swift; sourceTree = ""; }; + 84958C5D280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Performance.swift; sourceTree = ""; }; + 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelySegmentOption.swift; sourceTree = ""; }; + 84E2E9412852A378001114AB /* OdpVuidManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OdpVuidManager.swift; sourceTree = ""; }; + 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OptimizelySdkSettings.swift; path = ../ODP/OptimizelySdkSettings.swift; sourceTree = ""; }; + 84E2E9712855875E001114AB /* OdpEventManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OdpEventManager.swift; sourceTree = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; + 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP.swift; sourceTree = ""; }; + 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP_Decide.swift; sourceTree = ""; }; BD6485812491474500F30986 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C78CAF572445AD8D009FE876 /* OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyJSON.swift; sourceTree = ""; }; C78CAF652446DB91009FE876 /* OptimizelyClientTests_OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_OptimizelyJSON.swift; sourceTree = ""; }; @@ -2357,6 +2622,23 @@ path = "OptimizelyTests-MultiClients"; sourceTree = ""; }; + 6E6522B8278DF20F00954EA1 /* ODP */ = { + isa = PBXGroup; + children = ( + 6E6522DD278E4F3800954EA1 /* OdpManager.swift */, + 84E2E9412852A378001114AB /* OdpVuidManager.swift */, + 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */, + 84E2E9712855875E001114AB /* OdpEventManager.swift */, + 848617D82863E21200B7F41B /* OdpSegmentApiManager.swift */, + 848617D92863E21200B7F41B /* OdpEventApiManager.swift */, + 848617FA286CF33700B7F41B /* OdpEvent.swift */, + 84518B20287737070023F104 /* OdpConfig.swift */, + 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, + 6E6522FF278E688B00954EA1 /* LruCache.swift */, + ); + path = ODP; + sourceTree = ""; + }; 6E6BE008237F547200FE8274 /* optimizelyConfig */ = { isa = PBXGroup; children = ( @@ -2373,6 +2655,7 @@ 6EC6DD3F24ABF8180017D296 /* Optimizely+Decide */, 6E75165E22C520D400B2B157 /* Customization */, 6E75167C22C520D400B2B157 /* Implementation */, + 6E6522B8278DF20F00954EA1 /* ODP */, 6E75168822C520D400B2B157 /* Data Model */, 6E75169E22C520D400B2B157 /* Protocols */, 6E75167422C520D400B2B157 /* Extensions */, @@ -2410,6 +2693,7 @@ isa = PBXGroup; children = ( 6E75166722C520D400B2B157 /* OptimizelyError.swift */, + 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */, 6E75166822C520D400B2B157 /* OptimizelyLogLevel.swift */, 6E75166922C520D400B2B157 /* OptimizelyClient.swift */, 6E75166A22C520D400B2B157 /* OptimizelyClient+ObjC.swift */, @@ -2504,6 +2788,7 @@ 6E75168D22C520D400B2B157 /* ProjectConfig.swift */, 6E75168E22C520D400B2B157 /* FeatureVariable.swift */, 6E75168F22C520D400B2B157 /* Rollout.swift */, + 8464086F28130D3200CCF97D /* Integration.swift */, 6E75169022C520D400B2B157 /* Variation.swift */, 6E75169122C520D400B2B157 /* TrafficAllocation.swift */, 6E75169222C520D400B2B157 /* Project.swift */, @@ -2575,6 +2860,7 @@ 6E75196322C5211100B2B157 /* benchmark */, 6E6BE008237F547200FE8274 /* optimizelyConfig */, 6EF8DE0924B8DA5D008B9488 /* decide */, + 8464149E289B2D0700C45EE2 /* odp */, 6E75196222C5211100B2B157 /* optimizely_6372300739_v4.json */, 6E75196722C5211100B2B157 /* feature_rollout_toggle_on.json */, 6E75196822C5211100B2B157 /* feature_rollout_toggle_off.json */, @@ -2621,39 +2907,49 @@ 6E75197E22C5211100B2B157 /* OptimizelyTests-Common */ = { isa = PBXGroup; children = ( - 6E75197F22C5211100B2B157 /* MurmurTests.swift */, - 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */, - 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */, - 6E75198222C5211100B2B157 /* OptimizelySwiftSDKiOSTests.swift */, - 6E75198322C5211100B2B157 /* BucketTests_GroupToExp.swift */, - 6E75198422C5211100B2B157 /* BucketTests_Others.swift */, 6E75198522C5211100B2B157 /* BatchEventBuilderTest.swift */, - 6E75198622C5211100B2B157 /* DecisionServiceTests_UserProfiles.swift */, + 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */, 6E75198722C5211100B2B157 /* BatchEventBuilderTests_Events.swift */, - 6E75198822C5211100B2B157 /* DefaultLoggerTests.swift */, - 6E75198922C5211100B2B157 /* DefaultUserProfileServiceTests.swift */, + 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */, 6E75198A22C5211100B2B157 /* BucketTests_Base.swift */, - 6E75198B22C5211100B2B157 /* NotificationCenterTests.swift */, - 6E75198C22C5211100B2B157 /* BucketTests_ExpToVariation.swift */, - 6E75198E22C5211100B2B157 /* LoggerTests.swift */, 6E75198F22C5211100B2B157 /* BucketTests_BucketVariation.swift */, + 6E75198C22C5211100B2B157 /* BucketTests_ExpToVariation.swift */, + 6E75198322C5211100B2B157 /* BucketTests_GroupToExp.swift */, + 6E75198422C5211100B2B157 /* BucketTests_Others.swift */, + 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */, + 6E75199822C5211100B2B157 /* DataStoreTests.swift */, 6E75199022C5211100B2B157 /* DecisionListenerTests.swift */, 6E981FC1232C363300FADDD6 /* DecisionListenerTests_Datafile.swift */, + 6E27ECBD266FD78600B4A6D4 /* DecisionReasonsTests.swift */, + 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */, 6E75199122C5211100B2B157 /* DecisionServiceTests_Features.swift */, - 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */, - 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */, 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */, - 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */, + 6E75198622C5211100B2B157 /* DecisionServiceTests_UserProfiles.swift */, + 6E75198822C5211100B2B157 /* DefaultLoggerTests.swift */, + 6E75198922C5211100B2B157 /* DefaultUserProfileServiceTests.swift */, + 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */, + 6E75198E22C5211100B2B157 /* LoggerTests.swift */, + 8428D3CF2807337400D0FB0C /* LruCacheTests.swift */, + 6E75197F22C5211100B2B157 /* MurmurTests.swift */, 6E0207A7272A11CF008C3711 /* NetworkReachabilityTests.swift */, - 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */, - 6E75199822C5211100B2B157 /* DataStoreTests.swift */, - 6E27ECBD266FD78600B4A6D4 /* DecisionReasonsTests.swift */, + 6E75198B22C5211100B2B157 /* NotificationCenterTests.swift */, + 84861810286D0B8900B7F41B /* OdpEventManagerTests.swift */, + 8486180E286D0B8900B7F41B /* OdpManagerTests.swift */, + 8486180D286D0B8900B7F41B /* OdpSegmentManagerTests.swift */, + 8486180F286D0B8900B7F41B /* OdpVuidManagerTests.swift */, + 84861819286D188B00B7F41B /* OdpSegmentApiManagerTests.swift */, + 8486181A286D188B00B7F41B /* OdpEventApiManagerTests.swift */, 6E27EC9A266EF11000B4A6D4 /* OptimizelyDecisionTests.swift */, + 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */, 6EC6DD6824AE94820017D296 /* OptimizelyUserContextTests.swift */, 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */, - 6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */, 6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */, 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */, + 6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */, + 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */, + 84644AB228F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift */, + 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */, + 84958C5D280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift */, 6E86CEB224FF20DE005DAFED /* OptimizelyUserContextTests_Objc.m */, ); path = "OptimizelyTests-Common"; @@ -2677,6 +2973,7 @@ 6E7519A022C5211100B2B157 /* VariableTests.swift */, 6E7519A122C5211100B2B157 /* FeatureFlagTests.swift */, 6E7519A222C5211100B2B157 /* AudienceTests.swift */, + 84640880281320F000CCF97D /* IntegrationTests.swift */, 6E7519A322C5211100B2B157 /* ConditionLeafTests.swift */, 6E7519A422C5211100B2B157 /* AudienceTests_Evaluate.swift */, 6E7519A522C5211100B2B157 /* UserAttributeTests_Evaluate.swift */, @@ -2720,25 +3017,26 @@ 6E7519B922C5211100B2B157 /* OptimizelyTests-APIs */ = { isa = PBXGroup; children = ( + 6E7519BC22C5211100B2B157 /* OptimizelyErrorTests.swift */, 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */, 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */, + 6E7519C222C5211100B2B157 /* OptimizelyClientTests_Valid.swift */, + 6E7519BE22C5211100B2B157 /* OptimizelyClientTests_Invalid.swift */, + 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */, + 84518B1E287665020023F104 /* OptimizelyClientTests_ODP.swift */, 6E7519BA22C5211100B2B157 /* OptimizelyClientTests_Evaluation.swift */, 6E7519BB22C5211100B2B157 /* OptimizelyClientTests_DatafileHandler.swift */, - 6E7519BC22C5211100B2B157 /* OptimizelyErrorTests.swift */, - 6E7519BE22C5211100B2B157 /* OptimizelyClientTests_Invalid.swift */, - 6E7519BF22C5211100B2B157 /* OptimizelyClientTests_ObjcAPIs.m */, 6E7519C022C5211100B2B157 /* OptimizelyClientTests_Variables.swift */, - 6E7519C122C5211100B2B157 /* OptimizelyClientTests.swift */, - 6E7519C222C5211100B2B157 /* OptimizelyClientTests_Valid.swift */, - 6E7519C322C5211100B2B157 /* OptimizelyClientTests_Others.swift */, - 6E7519C422C5211100B2B157 /* OptimizelyClientTests_ForcedVariation.swift */, 6E7519C522C5211100B2B157 /* OptimizelyClientTests_Group.swift */, + 6E7519C422C5211100B2B157 /* OptimizelyClientTests_ForcedVariation.swift */, + C78CAF652446DB91009FE876 /* OptimizelyClientTests_OptimizelyJSON.swift */, + C78CAF8524485029009FE876 /* OptimizelyClientTests_OptimizelyJSON_Objc.m */, 6ECB60C5234D329500016D41 /* OptimizelyClientTests_OptimizelyConfig.swift */, 6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */, + 6E7519BF22C5211100B2B157 /* OptimizelyClientTests_ObjcAPIs.m */, 6E7519C622C5211100B2B157 /* OptimizelyClientTests_ObjcOthers.m */, - C78CAF652446DB91009FE876 /* OptimizelyClientTests_OptimizelyJSON.swift */, - C78CAF8524485029009FE876 /* OptimizelyClientTests_OptimizelyJSON_Objc.m */, - 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */, + 6E7519C322C5211100B2B157 /* OptimizelyClientTests_Others.swift */, + 6E7519C122C5211100B2B157 /* OptimizelyClientTests.swift */, ); path = "OptimizelyTests-APIs"; sourceTree = ""; @@ -2757,8 +3055,8 @@ children = ( 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */, 6EC6DD4024ABF89B0017D296 /* OptimizelyUserContext.swift */, - 6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */, 6EF8DE0A24BD1BB1008B9488 /* OptimizelyDecision.swift */, + 6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */, 6E86CEA124FDC836005DAFED /* OptimizelyUserContext+ObjC.swift */, ); path = "Optimizely+Decide"; @@ -2780,6 +3078,15 @@ path = watchOS; sourceTree = ""; }; + 8464149E289B2D0700C45EE2 /* odp */ = { + isa = PBXGroup; + children = ( + 8464149F289B2D0700C45EE2 /* decide_audience_segments.json */, + 846414A0289B2D0700C45EE2 /* odp_integrated_no_segments.json */, + ); + path = odp; + sourceTree = ""; + }; 87DE4DE091B80D1F13BBD781 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -3059,6 +3366,8 @@ dependencies = ( ); name = "OptimizelySwiftSDK-iOS"; + packageProductDependencies = ( + ); productName = OptimizelySwiftSDKiOS; productReference = 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */; productType = "com.apple.product-type.framework"; @@ -3184,6 +3493,8 @@ Base, ); mainGroup = 0B7CB0B821AC5FE2007B77E5; + packageReferences = ( + ); productRefGroup = 0B7CB0C321AC5FE2007B77E5 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -3240,7 +3551,9 @@ 6E14CDB72423FA0800010234 /* feature_management_experiment_bucketing.json in Resources */, 6E14CDBA2423FA0800010234 /* api_datafile.json in Resources */, 6E14CDC52423FA0800010234 /* bucketer_test.json in Resources */, + 846414A5289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E14CDC62423FA0800010234 /* bucketer_test2.json in Resources */, + 846414B1289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E14CDBF2423FA0800010234 /* typed_audience_datafile.json in Resources */, 6E14CDAE2423F9FC00010234 /* 50_entities.json in Resources */, 6E14CDB02423FA0800010234 /* optimizely_6372300739_v4.json in Resources */, @@ -3269,6 +3582,7 @@ 6EE592C4264DF4A70013AD66 /* 100_entities.json in Resources */, 6EE5929D264DF4990013AD66 /* feature_variables.json in Resources */, 6EE592B1264DF4990013AD66 /* forced_variation.json in Resources */, + 846414B0289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6EE592A1264DF4990013AD66 /* typed_audience_datafile.json in Resources */, 6EE59297264DF4990013AD66 /* bot_filtering_enabled.json in Resources */, 6EE592AB264DF4990013AD66 /* empty_datafile_new_project_id.json in Resources */, @@ -3283,6 +3597,7 @@ 6EE592AE264DF4990013AD66 /* bucketing_id.json in Resources */, 6EE59296264DF4990013AD66 /* feature_exp.json in Resources */, 6EE5929B264DF4990013AD66 /* audience_targeting.json in Resources */, + 846414A4289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6EE592A5264DF4990013AD66 /* feature_rollout_toggle_off.json in Resources */, 6EE592A3264DF4990013AD66 /* empty_datafile.json in Resources */, ); @@ -3326,7 +3641,9 @@ 6E12B26B22C55A290005E9E6 /* empty_datafile.json in Resources */, 6E12B26D22C55A290005E9E6 /* bucketer_test2.json in Resources */, 6E12B26A22C55A290005E9E6 /* feature_exp.json in Resources */, + 846414AA289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B25F22C55A290005E9E6 /* unsupported_datafile.json in Resources */, + 846414B6289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B26522C55A290005E9E6 /* feature_flag.json in Resources */, 6E12B26122C55A290005E9E6 /* feature_management_experiment_bucketing.json in Resources */, 6E12B2CD22C55A370005E9E6 /* 50_entities.json in Resources */, @@ -3345,6 +3662,7 @@ 6E12B20022C55A270005E9E6 /* rollout_bucketing.json in Resources */, 6E12B20222C55A270005E9E6 /* bucketing_id.json in Resources */, 6E12B2C022C55A340005E9E6 /* 10_entities.json in Resources */, + 846414B2289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B1FE22C55A270005E9E6 /* forced_variation.json in Resources */, 6E12B20422C55A270005E9E6 /* api_datafile.json in Resources */, 6E12B21022C55A270005E9E6 /* simple_datafile.json in Resources */, @@ -3357,6 +3675,7 @@ 6EA0FB22251A5AEC00EC002D /* bucketer_test3.json in Resources */, 6E12B20E22C55A270005E9E6 /* ab_experiments.json in Resources */, 6E12B20622C55A270005E9E6 /* grouped_experiments.json in Resources */, + 846414A6289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E34A629231ED04900BAE302 /* empty_datafile_new_project_id.json in Resources */, 6E12B20322C55A270005E9E6 /* feature_variables.json in Resources */, 6E12B20822C55A270005E9E6 /* audience_targeting.json in Resources */, @@ -3407,7 +3726,9 @@ 6E12B25322C55A280005E9E6 /* empty_datafile.json in Resources */, 6E12B25522C55A280005E9E6 /* bucketer_test2.json in Resources */, 6E12B25222C55A280005E9E6 /* feature_exp.json in Resources */, + 846414A9289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B24722C55A280005E9E6 /* unsupported_datafile.json in Resources */, + 846414B5289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B24D22C55A280005E9E6 /* feature_flag.json in Resources */, 6E12B24922C55A280005E9E6 /* feature_management_experiment_bucketing.json in Resources */, 6E12B2CA22C55A360005E9E6 /* 50_entities.json in Resources */, @@ -3436,6 +3757,7 @@ 6EF8DE0824B8DA58008B9488 /* decide_datafile.json in Resources */, 6E12B27E22C55A290005E9E6 /* grouped_experiments.json in Resources */, 6E34A62E231ED04900BAE302 /* empty_datafile_new_project_id.json in Resources */, + 846414B7289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B27B22C55A290005E9E6 /* feature_variables.json in Resources */, 6E12B28022C55A290005E9E6 /* audience_targeting.json in Resources */, 6E12B27322C55A290005E9E6 /* feature_rollout_toggle_on.json in Resources */, @@ -3450,6 +3772,7 @@ 6E12B27722C55A290005E9E6 /* unsupported_datafile.json in Resources */, 6E12B27D22C55A290005E9E6 /* feature_flag.json in Resources */, 6E12B27922C55A290005E9E6 /* feature_management_experiment_bucketing.json in Resources */, + 846414AB289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B2D022C55A370005E9E6 /* 50_entities.json in Resources */, 6E34A647231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */, ); @@ -3476,6 +3799,7 @@ 6E12B29E22C55A2A0005E9E6 /* ab_experiments.json in Resources */, 6E12B29622C55A2A0005E9E6 /* grouped_experiments.json in Resources */, 6E34A62F231ED04900BAE302 /* empty_datafile_new_project_id.json in Resources */, + 846414B8289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B29322C55A2A0005E9E6 /* feature_variables.json in Resources */, 6E12B29822C55A2A0005E9E6 /* audience_targeting.json in Resources */, 6E12B28B22C55A2A0005E9E6 /* feature_rollout_toggle_on.json in Resources */, @@ -3490,6 +3814,7 @@ 6E12B28F22C55A2A0005E9E6 /* unsupported_datafile.json in Resources */, 6E12B29522C55A2A0005E9E6 /* feature_flag.json in Resources */, 6E12B29122C55A2A0005E9E6 /* feature_management_experiment_bucketing.json in Resources */, + 846414AC289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B2D322C55A380005E9E6 /* 50_entities.json in Resources */, 6E34A648231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */, ); @@ -3516,6 +3841,7 @@ 6EF8DE0624B8DA58008B9488 /* decide_datafile.json in Resources */, 6E12B1EE22C55A260005E9E6 /* grouped_experiments.json in Resources */, 6E34A628231ED04900BAE302 /* empty_datafile_new_project_id.json in Resources */, + 846414AF289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B1EB22C55A260005E9E6 /* feature_variables.json in Resources */, 6E12B1F022C55A260005E9E6 /* audience_targeting.json in Resources */, 6E12B1E322C55A260005E9E6 /* feature_rollout_toggle_on.json in Resources */, @@ -3530,6 +3856,7 @@ 6E12B1E722C55A260005E9E6 /* unsupported_datafile.json in Resources */, 6E12B1ED22C55A260005E9E6 /* feature_flag.json in Resources */, 6E12B1E922C55A260005E9E6 /* feature_management_experiment_bucketing.json in Resources */, + 846414A3289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B2BE22C55A330005E9E6 /* 50_entities.json in Resources */, 6E34A641231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */, ); @@ -3556,6 +3883,7 @@ 6E12B22622C55A270005E9E6 /* ab_experiments.json in Resources */, 6E12B21E22C55A270005E9E6 /* grouped_experiments.json in Resources */, 6E34A62A231ED04900BAE302 /* empty_datafile_new_project_id.json in Resources */, + 846414B3289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B21B22C55A270005E9E6 /* feature_variables.json in Resources */, 6E12B22022C55A270005E9E6 /* audience_targeting.json in Resources */, 6E12B21322C55A270005E9E6 /* feature_rollout_toggle_on.json in Resources */, @@ -3570,6 +3898,7 @@ 6E12B21722C55A270005E9E6 /* unsupported_datafile.json in Resources */, 6E12B21D22C55A270005E9E6 /* feature_flag.json in Resources */, 6E12B21922C55A270005E9E6 /* feature_management_experiment_bucketing.json in Resources */, + 846414A7289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B2C422C55A350005E9E6 /* 50_entities.json in Resources */, 6E34A643231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */, ); @@ -3606,7 +3935,9 @@ 6E12B23B22C55A280005E9E6 /* empty_datafile.json in Resources */, 6E12B23D22C55A280005E9E6 /* bucketer_test2.json in Resources */, 6E12B23A22C55A280005E9E6 /* feature_exp.json in Resources */, + 846414A8289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B22F22C55A280005E9E6 /* unsupported_datafile.json in Resources */, + 846414B4289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B23522C55A280005E9E6 /* feature_flag.json in Resources */, 6E12B23122C55A280005E9E6 /* feature_management_experiment_bucketing.json in Resources */, 6E12B2C722C55A350005E9E6 /* 50_entities.json in Resources */, @@ -3645,7 +3976,9 @@ 6E12B2B322C55A2A0005E9E6 /* empty_datafile.json in Resources */, 6E12B2B522C55A2B0005E9E6 /* bucketer_test2.json in Resources */, 6E12B2B222C55A2A0005E9E6 /* feature_exp.json in Resources */, + 846414AD289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B2A722C55A2A0005E9E6 /* unsupported_datafile.json in Resources */, + 846414B9289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B2AD22C55A2A0005E9E6 /* feature_flag.json in Resources */, 6E12B2A922C55A2A0005E9E6 /* feature_management_experiment_bucketing.json in Resources */, 6E12B2D622C55A380005E9E6 /* 50_entities.json in Resources */, @@ -3691,7 +4024,9 @@ 6E12B1DB22C55A250005E9E6 /* empty_datafile.json in Resources */, 6E12B1DD22C55A250005E9E6 /* bucketer_test2.json in Resources */, 6E12B1DA22C55A250005E9E6 /* feature_exp.json in Resources */, + 846414A2289B2D0700C45EE2 /* decide_audience_segments.json in Resources */, 6E12B1CF22C55A250005E9E6 /* unsupported_datafile.json in Resources */, + 846414AE289B2D0700C45EE2 /* odp_integrated_no_segments.json in Resources */, 6E12B1D522C55A250005E9E6 /* feature_flag.json in Resources */, 6E12B1D122C55A250005E9E6 /* feature_management_experiment_bucketing.json in Resources */, 6E12B2BB22C55A330005E9E6 /* 50_entities.json in Resources */, @@ -3769,15 +4104,20 @@ files = ( 6E14CDAB2423F9EB00010234 /* MockUrlSession.swift in Sources */, 6E14CDAA2423F9C300010234 /* SDKVersion.swift in Sources */, + 845945C3287758A100D13E11 /* OdpConfig.swift in Sources */, 6E14CD832423F9A100010234 /* DataStoreQueueStackImpl.swift in Sources */, + 848617F12863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E14CD812423F9A100010234 /* DataStoreUserDefaults.swift in Sources */, 6E14CD802423F9A100010234 /* DataStoreMemory.swift in Sources */, 6E14CDA02423F9C300010234 /* OptimizelyClient+Extension.swift in Sources */, 6E14CDA22423F9C300010234 /* Array+Extension.swift in Sources */, + 848617CF2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E14CD952423F9A700010234 /* Group.swift in Sources */, + 84E2E96828540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E14CD9A2423F9C300010234 /* DataStoreQueueStack.swift in Sources */, 6E14CD732423F96F00010234 /* OptimizelyResult.swift in Sources */, 6E14CD7E2423F98D00010234 /* DefaultNotificationCenter.swift in Sources */, + 84861802286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E14CD8B2423F9A100010234 /* UserAttribute.swift in Sources */, 84E7ABC227D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E14CD702423F94800010234 /* OptimizelyLogLevel.swift in Sources */, @@ -3805,12 +4145,14 @@ C78CAFA824486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E14CD932423F9A700010234 /* Experiment.swift in Sources */, 6E14CD982423F9C300010234 /* BackgroundingCallbacks.swift in Sources */, + 6E6522E5278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E623F07253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5C2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E14CDA52423F9C300010234 /* MurmurHash3.swift in Sources */, 6E86CEA824FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E5D12222638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E14CD912423F9A700010234 /* TrafficAllocation.swift in Sources */, + 84B4D75727E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E14CD6F2423F93E00010234 /* OptimizelyError.swift in Sources */, 6EC6DD3624ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E14CD752423F97600010234 /* OptimizelyConfig+ObjC.swift in Sources */, @@ -3820,21 +4162,25 @@ 6E14CDA92423F9C300010234 /* Utils.swift in Sources */, 6EF8DE1F24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E14CD882423F9A100010234 /* AttributeValue.swift in Sources */, + 84E2E9492852A378001114AB /* OdpVuidManager.swift in Sources */, 6E14CD822423F9A100010234 /* DataStoreFile.swift in Sources */, 6E14CDA42423F9C300010234 /* Notifications.swift in Sources */, 6E20050B26B4D28500278087 /* MockLogger.swift in Sources */, 6E14CD872423F9A100010234 /* Audience.swift in Sources */, 6E14CD7D2423F98D00010234 /* DefaultBucketer.swift in Sources */, 6E14CD792423F98D00010234 /* OPTLogger.swift in Sources */, + 848617E12863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E14CD992423F9C300010234 /* OPTNotificationCenter.swift in Sources */, 6E14CD722423F96B00010234 /* OptimizelyClient+ObjC.swift in Sources */, 6E14CDA62423F9C300010234 /* HandlerRegistryService.swift in Sources */, 6E14CD7B2423F98D00010234 /* OPTEventDispatcher.swift in Sources */, 6E14CD902423F9A700010234 /* Variation.swift in Sources */, 6E14CD8E2423F9A700010234 /* FeatureVariable.swift in Sources */, + 8464087728130D3200CCF97D /* Integration.swift in Sources */, 6E14CD8D2423F9A700010234 /* ProjectConfig.swift in Sources */, 0B97DD9F249D4A23003DE606 /* SemanticVersion.swift in Sources */, 6E14CD8F2423F9A700010234 /* Rollout.swift in Sources */, + 84E2E9792855875E001114AB /* OdpEventManager.swift in Sources */, 6E14CD892423F9A100010234 /* ConditionLeaf.swift in Sources */, 6E14CD9F2423F9C300010234 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E14CD9C2423F9C300010234 /* OPTDecisionService.swift in Sources */, @@ -3848,6 +4194,7 @@ 6E14CD7A2423F98D00010234 /* OPTUserProfileService.swift in Sources */, 6E14CDA32423F9C300010234 /* Constants.swift in Sources */, 6E4544B1270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 6E652307278E688B00954EA1 /* LruCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3868,14 +4215,18 @@ 6E424CF426324B620081004A /* DecisionInfo.swift in Sources */, 6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */, 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, + 848617E02863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */, + 84E2E96728540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */, 6E424CF626324B620081004A /* DefaultNotificationCenter.swift in Sources */, 6E424CF726324B620081004A /* DefaultDecisionService.swift in Sources */, 6E424CF826324B620081004A /* DecisionReasons.swift in Sources */, 6E424CF926324B620081004A /* DecisionResponse.swift in Sources */, + 84E2E9782855875E001114AB /* OdpEventManager.swift in Sources */, 6E424CFA26324B620081004A /* DataStoreMemory.swift in Sources */, 6E424CFB26324B620081004A /* DataStoreUserDefaults.swift in Sources */, + 6E6522E4278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */, 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */, 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */, @@ -3884,6 +4235,7 @@ 6E424D0126324B620081004A /* SemanticVersion.swift in Sources */, 6E424D0226324B620081004A /* Audience.swift in Sources */, 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, + 84E2E9482852A378001114AB /* OdpVuidManager.swift in Sources */, 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */, 6E424D0526324B620081004A /* ConditionHolder.swift in Sources */, 6E424D5226324C4D0081004A /* OptimizelyClient+Decide.swift in Sources */, @@ -3905,6 +4257,7 @@ 6E424D1126324B620081004A /* Variable.swift in Sources */, 6E424D1226324B620081004A /* Attribute.swift in Sources */, 6E424D1326324B620081004A /* BackgroundingCallbacks.swift in Sources */, + 845945C2287758A000D13E11 /* OdpConfig.swift in Sources */, 6E424D1426324B620081004A /* OPTNotificationCenter.swift in Sources */, 6E424D5026324C4D0081004A /* OptimizelyDecideOption.swift in Sources */, 6E424D5126324C4D0081004A /* OptimizelyDecision.swift in Sources */, @@ -3918,8 +4271,10 @@ 6E424D1A26324B620081004A /* OptimizelyClient+Extension.swift in Sources */, 6E424D1B26324B620081004A /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E424D1C26324B620081004A /* Array+Extension.swift in Sources */, + 84B4D75627E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E424D7626324DBD0081004A /* AtomicArrayTests.swift in Sources */, 6E424CD326324B270081004A /* OptimizelyError.swift in Sources */, + 6E652306278E688B00954EA1 /* LruCache.swift in Sources */, 6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */, 6E424CD426324B270081004A /* OptimizelyLogLevel.swift in Sources */, 6EE5918E264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift in Sources */, @@ -3934,9 +4289,13 @@ 6E424CB926324B1D0081004A /* Constants.swift in Sources */, 6E424CBA26324B1D0081004A /* Notifications.swift in Sources */, 6E424CBB26324B1D0081004A /* MurmurHash3.swift in Sources */, + 8464087628130D3200CCF97D /* Integration.swift in Sources */, + 848617CE2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, + 848617F02863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E424CBC26324B1D0081004A /* HandlerRegistryService.swift in Sources */, 6E424CBD26324B1D0081004A /* LogMessage.swift in Sources */, 6E424CBE26324B1D0081004A /* AtomicProperty.swift in Sources */, + 84861801286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E2F8AFF26B22E8000DCEEB9 /* ConcurrencyTests_SingleClient.swift in Sources */, 84E7ABC127D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E424CBF26324B1D0081004A /* AtomicArray.swift in Sources */, @@ -3958,13 +4317,16 @@ 6E7518C522C520D400B2B157 /* Audience.swift in Sources */, 6E7517BD22C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E7518F522C520D500B2B157 /* UserAttribute.swift in Sources */, + 84E2E9732855875E001114AB /* OdpEventManager.swift in Sources */, 6EF8DE0D24BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6EF8DE3224BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75192522C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7516FB22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 84E7ABBC27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E75184D22C520D400B2B157 /* ProjectConfig.swift in Sources */, + 8464087128130D3200CCF97D /* Integration.swift in Sources */, 6E623F03253F9045000617D0 /* DecisionInfo.swift in Sources */, + 845945BD2877589E00D13E11 /* OdpConfig.swift in Sources */, 6E75171322C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75191922C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E7518A122C520D400B2B157 /* FeatureFlag.swift in Sources */, @@ -3979,7 +4341,9 @@ 6E75186522C520D400B2B157 /* Rollout.swift in Sources */, 6E86CEA324FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75190122C520D500B2B157 /* Attribute.swift in Sources */, + 848617DB2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E7516B322C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 84E2E9432852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E7517D522C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75187122C520D400B2B157 /* Variation.swift in Sources */, @@ -3993,9 +4357,11 @@ 6E75185922C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E4544AB270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E7517ED22C520D400B2B157 /* DataStoreMemory.swift in Sources */, + 848617EB2863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E75172B22C520D400B2B157 /* Constants.swift in Sources */, 6E7516BF22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E424BDE263228E90081004A /* AtomicArray.swift in Sources */, + 848617FC286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E7517E122C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75178B22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E75177F22C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4004,6 +4370,7 @@ 6E75173722C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E7517F922C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, C78CAF592445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, + 848617C92863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E7518E922C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E75184122C520D400B2B157 /* Event.swift in Sources */, 6E7517C922C520D400B2B157 /* DefaultBucketer.swift in Sources */, @@ -4011,6 +4378,7 @@ 6E7516CB22C520D400B2B157 /* OPTLogger.swift in Sources */, 6E7517A322C520D400B2B157 /* Array+Extension.swift in Sources */, 6EC6DD4224ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, + 6E652301278E688B00954EA1 /* LruCache.swift in Sources */, 6E75193122C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75190D22C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75194922C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -4018,9 +4386,12 @@ 6E75174F22C520D400B2B157 /* LogMessage.swift in Sources */, 6E75189522C520D400B2B157 /* Experiment.swift in Sources */, 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */, + 84B4D75127E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 6E6522DF278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E7518D122C520D400B2B157 /* AttributeValue.swift in Sources */, 6E75182922C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75171F22C520D400B2B157 /* OptimizelyResult.swift in Sources */, + 84E2E96228540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75170722C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75174322C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E75188922C520D400B2B157 /* Project.swift in Sources */, @@ -4037,15 +4408,20 @@ files = ( 6E75170222C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E7516BA22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 845945C7287758A300D13E11 /* OdpConfig.swift in Sources */, 6E75175622C520D400B2B157 /* LogMessage.swift in Sources */, + 848617F62863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E75193822C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75191422C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75172622C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75173222C520D400B2B157 /* Constants.swift in Sources */, + 848617D42863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75184822C520D400B2B157 /* Event.swift in Sources */, + 84E2E96D28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75170E22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177A22C520D400B2B157 /* SDKVersion.swift in Sources */, 6E7516C622C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 84861807286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75189C22C520D400B2B157 /* Experiment.swift in Sources */, 84E7ABC727D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E75176222C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -4073,12 +4449,14 @@ C78CAFAD24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7516AE22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75195022C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, + 6E6522EA278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E623F0C253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF612445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516D222C520D400B2B157 /* OPTLogger.swift in Sources */, 6E86CEAC24FDC849005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E5D12272638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75186022C520D400B2B157 /* FeatureVariable.swift in Sources */, + 84B4D75C27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E7517E822C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6EC6DD3B24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E7516DE22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, @@ -4088,21 +4466,25 @@ 6E7518F022C520D500B2B157 /* ConditionHolder.swift in Sources */, 6EF8DE2424BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75183022C520D400B2B157 /* BatchEvent.swift in Sources */, + 84E2E94E2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75192022C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E9B117922C5487A00C22D81 /* tvOSOnlyTests.swift in Sources */, 6E20051026B4D28500278087 /* MockLogger.swift in Sources */, 6E7518B422C520D400B2B157 /* Group.swift in Sources */, 6E7518CC22C520D400B2B157 /* Audience.swift in Sources */, 6E75176E22C520D400B2B157 /* Utils.swift in Sources */, + 848617E62863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E75182422C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E75174A22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E7516F622C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75188422C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6EA2CC2C2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E7517D022C520D400B2B157 /* DefaultBucketer.swift in Sources */, + 8464087C28130D3200CCF97D /* Integration.swift in Sources */, 6E75180022C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 0B97DDA3249D4A26003DE606 /* SemanticVersion.swift in Sources */, 6E9B11B322C5489500C22D81 /* MockUrlSession.swift in Sources */, + 84E2E97E2855875E001114AB /* OdpEventManager.swift in Sources */, 6E7517DC22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75178622C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E75171A22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -4116,6 +4498,7 @@ 6E75194422C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75179222C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E4544B6270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 6E65230C278E688B00954EA1 /* LruCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4127,6 +4510,7 @@ 6EF8DE2024BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7517D822C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75177622C520D400B2B157 /* SDKVersion.swift in Sources */, + 84518B1F287665020023F104 /* OptimizelyClientTests_ODP.swift in Sources */, 6E7516FE22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75173A22C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E7517CC22C520D400B2B157 /* DefaultBucketer.swift in Sources */, @@ -4143,16 +4527,19 @@ 6E75181422C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */, 6E7516C222C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 848617F22863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E75188022C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E9B11DD22C548A200C22D81 /* OptimizelyClientTests_Valid.swift in Sources */, 0B97DDA0249D4A24003DE606 /* SemanticVersion.swift in Sources */, 6E7518D422C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7518BC22C520D400B2B157 /* Variable.swift in Sources */, + 84861803286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75192822C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7516B622C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6ECB60D7234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m in Sources */, 6E75195822C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E424C03263228FD0081004A /* AtomicDictionary.swift in Sources */, + 84E2E94A2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E994B3A25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75170A22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E9B11AC22C5489300C22D81 /* OTUtils.swift in Sources */, @@ -4160,6 +4547,7 @@ 6E75180822C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7518EC22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E7516AA22C520D400B2B157 /* DefaultLogger.swift in Sources */, + 84E2E97A2855875E001114AB /* OdpEventManager.swift in Sources */, 6E75186822C520D400B2B157 /* Rollout.swift in Sources */, 6E9B11E122C548A200C22D81 /* OptimizelyClientTests_ObjcOthers.m in Sources */, 6E623F08253F9045000617D0 /* DecisionInfo.swift in Sources */, @@ -4171,15 +4559,18 @@ 6E9B11D722C548A200C22D81 /* OptimizelyErrorTests.swift in Sources */, C78CAF8624485029009FE876 /* OptimizelyClientTests_OptimizelyJSON_Objc.m in Sources */, 6E75194C22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, + 6E652308278E688B00954EA1 /* LruCache.swift in Sources */, 6E7517C022C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75183822C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75175222C520D400B2B157 /* LogMessage.swift in Sources */, 6E9B11DB22C548A200C22D81 /* OptimizelyClientTests_Variables.swift in Sources */, + 8464087828130D3200CCF97D /* Integration.swift in Sources */, 6E5D12232638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6ECB60C6234D329500016D41 /* OptimizelyClientTests_OptimizelyConfig.swift in Sources */, 6EA2CC282345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E75189822C520D400B2B157 /* Experiment.swift in Sources */, 6E8A3D4C2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, + 84B4D75827E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E9B11DF22C548A200C22D81 /* OptimizelyClientTests_ForcedVariation.swift in Sources */, 6E7516DA22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E34A61B2319EBB800BAE302 /* Notifications.swift in Sources */, @@ -4188,22 +4579,26 @@ 6E7517FC22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E75178222C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7518A422C520D400B2B157 /* FeatureFlag.swift in Sources */, + 6E6522E6278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E75185C22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E20050C26B4D28500278087 /* MockLogger.swift in Sources */, 6E75176A22C520D400B2B157 /* Utils.swift in Sources */, 6E75171622C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E7517F022C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B11D922C548A200C22D81 /* OptimizelyClientTests_Invalid.swift in Sources */, + 848617D02863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E9B11D522C548A200C22D81 /* OptimizelyClientTests_Evaluation.swift in Sources */, 6E5AB69423F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift in Sources */, 6EF8DE1224BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E9B11DA22C548A200C22D81 /* OptimizelyClientTests_ObjcAPIs.m in Sources */, + 84518B21287737070023F104 /* OdpConfig.swift in Sources */, 6E75179A22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E75182022C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E5AB69323F6130D007A82B1 /* OptimizelyClientTests_Init_Sync.swift in Sources */, 6E4544B2270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75184422C520D400B2B157 /* Event.swift in Sources */, 6E75194022C520D500B2B157 /* OPTDecisionService.swift in Sources */, + 848617E22863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E7518E022C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E7518B022C520D400B2B157 /* Group.swift in Sources */, 6E75185022C520D400B2B157 /* ProjectConfig.swift in Sources */, @@ -4211,6 +4606,7 @@ 6E9B11AB22C5489300C22D81 /* MockUrlSession.swift in Sources */, 6E75190422C520D500B2B157 /* Attribute.swift in Sources */, 6E75193422C520D500B2B157 /* OPTDataStore.swift in Sources */, + 84E2E96928540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75182C22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75175E22C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E9B11DE22C548A200C22D81 /* OptimizelyClientTests_Others.swift in Sources */, @@ -4232,6 +4628,8 @@ files = ( 6EC6DD4A24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75170122C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, + 8464087B28130D3200CCF97D /* Integration.swift in Sources */, + 84E2E96C28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6EF8DE3A24BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E7516B922C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E75175522C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4246,11 +4644,15 @@ 6E994B3D25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75170D22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177922C520D400B2B157 /* SDKVersion.swift in Sources */, + 848617F52863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 0B97DDA2249D4A25003DE606 /* SemanticVersion.swift in Sources */, + 848617E52863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E7516C522C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75189B22C520D400B2B157 /* Experiment.swift in Sources */, + 845945C6287758A300D13E11 /* OdpConfig.swift in Sources */, 6E75176122C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75180B22C520D400B2B157 /* DataStoreFile.swift in Sources */, + 6E6522E9278E4F3800954EA1 /* OdpManager.swift in Sources */, 6EF8DE2324BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7517C322C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190722C520D500B2B157 /* Attribute.swift in Sources */, @@ -4258,18 +4660,21 @@ 6E7518BF22C520D400B2B157 /* Variable.swift in Sources */, 6E75181722C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75185322C520D400B2B157 /* ProjectConfig.swift in Sources */, + 84E2E94D2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75173D22C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E7516E922C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7518A722C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75187722C520D400B2B157 /* Variation.swift in Sources */, 6E7517F322C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E7518FB22C520D500B2B157 /* UserAttribute.swift in Sources */, + 6E65230B278E688B00954EA1 /* LruCache.swift in Sources */, 6E9B11B222C5489400C22D81 /* OTUtils.swift in Sources */, 6E7518D722C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516AD22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75194F22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7516D122C520D400B2B157 /* OPTLogger.swift in Sources */, 6E75185F22C520D400B2B157 /* FeatureVariable.swift in Sources */, + 84B4D75B27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E7517E722C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E424BE7263228E90081004A /* AtomicArray.swift in Sources */, 6E7516DD22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, @@ -4299,6 +4704,7 @@ 6E7517CF22C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6E7517FF22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E34A61E2319EBB800BAE302 /* Notifications.swift in Sources */, + 848617D32863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E8A3D4F2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E4544B5270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E7517DB22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, @@ -4309,6 +4715,8 @@ 6E75186B22C520D400B2B157 /* Rollout.swift in Sources */, 6E75183B22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75194322C520D500B2B157 /* OPTDecisionService.swift in Sources */, + 84E2E97D2855875E001114AB /* OdpEventManager.swift in Sources */, + 84861806286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75179122C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4319,15 +4727,20 @@ files = ( 6E7E9B382523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift in Sources */, 6E9B117022C5487100C22D81 /* DecisionListenerTests.swift in Sources */, + 848617F72863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E9B116122C5487100C22D81 /* OptimizelyErrorTests.swift in Sources */, 6E9B116522C5487100C22D81 /* BatchEventBuilderTest.swift in Sources */, + 84861808286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E9B117422C5487100C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B116E22C5487100C22D81 /* LoggerTests.swift in Sources */, + 84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift in Sources */, 6E75180D22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E75178722C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E75179F22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7516BB22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E424C08263228FD0081004A /* AtomicDictionary.swift in Sources */, + 84861818286D0B8900B7F41B /* OdpEventManagerTests.swift in Sources */, + 84958C5F280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift in Sources */, 6E86CEAD24FDC84A005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75184922C520D400B2B157 /* Event.swift in Sources */, 0B97DDA4249D4A27003DE606 /* SemanticVersion.swift in Sources */, @@ -4338,6 +4751,7 @@ 6E7518C122C520D400B2B157 /* Variable.swift in Sources */, 6E75170F22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6EC6DD4C24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, + 8464087D28130D3200CCF97D /* Integration.swift in Sources */, 6E7517E922C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E5D12282638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E9B116A22C5487100C22D81 /* BucketTests_Base.swift in Sources */, @@ -4348,9 +4762,12 @@ 6EF8DE2524BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75194522C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185522C520D400B2B157 /* ProjectConfig.swift in Sources */, + 84F6BAB427FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift in Sources */, 6E7516C722C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E9B11B522C5489600C22D81 /* MockUrlSession.swift in Sources */, 6EF8DE1724BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, + 8428D3D12807337400D0FB0C /* LruCacheTests.swift in Sources */, + 84861816286D0B8900B7F41B /* OdpVuidManagerTests.swift in Sources */, 6E7517C522C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190922C520D500B2B157 /* Attribute.swift in Sources */, 6E75177B22C520D400B2B157 /* SDKVersion.swift in Sources */, @@ -4364,15 +4781,16 @@ 6E7516EB22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75188522C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E75176F22C520D400B2B157 /* Utils.swift in Sources */, + 6E65230D278E688B00954EA1 /* LruCache.swift in Sources */, 6E75182522C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6EC6DD6A24AE94820017D296 /* OptimizelyUserContextTests.swift in Sources */, 6E7517D122C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6EA2CC2D2345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFAE24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7517AB22C520D400B2B157 /* Array+Extension.swift in Sources */, + 8486181E286D188B00B7F41B /* OdpEventApiManagerTests.swift in Sources */, 6E75186122C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E75172722C520D400B2B157 /* OptimizelyResult.swift in Sources */, - 6E9B116222C5487100C22D81 /* OptimizelySwiftSDKiOSTests.swift in Sources */, 6E7518FD22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7518E522C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E623F0D253F9045000617D0 /* DecisionInfo.swift in Sources */, @@ -4380,11 +4798,14 @@ 6E9B117222C5487100C22D81 /* EventDispatcherTests.swift in Sources */, 6E9B116922C5487100C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E75192122C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, + 84861814286D0B8900B7F41B /* OdpManagerTests.swift in Sources */, 6E75170322C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E8A3D512637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E981FC3232C363300FADDD6 /* DecisionListenerTests_Datafile.swift in Sources */, + 845945C8287758A500D13E11 /* OdpConfig.swift in Sources */, 6E9B116422C5487100C22D81 /* BucketTests_Others.swift in Sources */, 6E7518CD22C520D400B2B157 /* Audience.swift in Sources */, + 84E2E96E28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E9B117322C5487100C22D81 /* BatchEventBuilderTests_Attributes.swift in Sources */, 6E9B11B622C5489600C22D81 /* OTUtils.swift in Sources */, 6E75183122C520D400B2B157 /* BatchEvent.swift in Sources */, @@ -4392,13 +4813,16 @@ 6E9B117822C5487100C22D81 /* DataStoreTests.swift in Sources */, 6E75171B22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75195122C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, + 84E2E94F2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75176322C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E9B117722C5487100C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */, 6E7517DD22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E9B116622C5487100C22D81 /* DecisionServiceTests_UserProfiles.swift in Sources */, 6E34A6202319EBB800BAE302 /* Notifications.swift in Sources */, 6E75173322C520D400B2B157 /* Constants.swift in Sources */, + 8486181C286D188B00B7F41B /* OdpSegmentApiManagerTests.swift in Sources */, 6E7518A922C520D400B2B157 /* FeatureFlag.swift in Sources */, + 84B4D75D27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E0A72D526C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift in Sources */, 6E75173F22C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E75189D22C520D400B2B157 /* Experiment.swift in Sources */, @@ -4409,16 +4833,21 @@ 6E75179322C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B117122C5487100C22D81 /* DecisionServiceTests_Features.swift in Sources */, 6E9B116F22C5487100C22D81 /* BucketTests_BucketVariation.swift in Sources */, + 848617E72863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E994B3F25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75174B22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, + 6E6522EB278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E75187922C520D400B2B157 /* Variation.swift in Sources */, 6E75191522C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75195D22C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E9B117622C5487100C22D81 /* DatafileHandlerTests.swift in Sources */, + 84E2E97F2855875E001114AB /* OdpEventManager.swift in Sources */, 6E9B116722C5487100C22D81 /* BatchEventBuilderTests_Events.swift in Sources */, 6E75181922C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, + 84644AB428F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift in Sources */, 6E75186D22C520D400B2B157 /* Rollout.swift in Sources */, 6E4544B7270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 848617D52863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E7518F122C520D500B2B157 /* ConditionHolder.swift in Sources */, 6E20051126B4D28600278087 /* MockLogger.swift in Sources */, 6E7516DF22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, @@ -4426,6 +4855,7 @@ 6E7518B522C520D400B2B157 /* Group.swift in Sources */, 6E9B116B22C5487100C22D81 /* NotificationCenterTests.swift in Sources */, 6E7516F722C520D400B2B157 /* OptimizelyError.swift in Sources */, + 84861812286D0B8900B7F41B /* OdpSegmentManagerTests.swift in Sources */, 6E75189122C520D400B2B157 /* Project.swift in Sources */, 6E7517F522C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E0207A9272A11CF008C3711 /* NetworkReachabilityTests.swift in Sources */, @@ -4449,17 +4879,21 @@ 6E75170422C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187A22C520D400B2B157 /* Variation.swift in Sources */, 0B97DD9A249D332C003DE606 /* SemanticVersionTests.swift in Sources */, + 6E65230E278E688B00954EA1 /* LruCache.swift in Sources */, 6E9B119C22C5488300C22D81 /* ProjectConfigTests.swift in Sources */, 6E7518FE22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7517F622C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B119322C5488300C22D81 /* AttributeTests.swift in Sources */, + 845945C9287758A600D13E11 /* OdpConfig.swift in Sources */, 6EC6DD3D24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E9B11A122C5488300C22D81 /* EventTests.swift in Sources */, 6E75188622C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E9B119522C5488300C22D81 /* FeatureFlagTests.swift in Sources */, 6E75190A22C520D500B2B157 /* Attribute.swift in Sources */, + 84861809286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75171022C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C822C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 84E2E9502852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75194622C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185622C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20051226B4D28600278087 /* MockLogger.swift in Sources */, @@ -4478,21 +4912,25 @@ 6E7516D422C520D400B2B157 /* OPTLogger.swift in Sources */, 6E75183222C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7518DA22C520D400B2B157 /* AttributeValue.swift in Sources */, + 84640882281320F000CCF97D /* IntegrationTests.swift in Sources */, 6E9B119822C5488300C22D81 /* AudienceTests_Evaluate.swift in Sources */, 6E75192222C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75177022C520D400B2B157 /* Utils.swift in Sources */, 6E7516E022C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E34A6212319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B119D22C5488300C22D81 /* UserAttributeTests.swift in Sources */, + 848617F82863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E5D12292638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75183E22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B11A022C5488300C22D81 /* ExperimentTests.swift in Sources */, 6E7516EC22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75181A22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE2624BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 8464087E28130D3200CCF97D /* Integration.swift in Sources */, 6E9B119722C5488300C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184A22C520D400B2B157 /* Event.swift in Sources */, 6E75191622C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, + 848617D62863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E9B11A522C5488300C22D81 /* ConditionHolderTests_Evaluate.swift in Sources */, 84E7ABC927D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E9B119122C5488300C22D81 /* EventForDispatchTests.swift in Sources */, @@ -4506,12 +4944,14 @@ 6E7518E622C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75179422C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11B722C5489600C22D81 /* MockUrlSession.swift in Sources */, + 6E6522EC278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E9B119222C5488300C22D81 /* FeatureVariableTests.swift in Sources */, 6E75176422C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF632445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516BC22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E623F0E253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E4544B8270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 84E2E96F28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E7517A022C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517AC22C520D400B2B157 /* Array+Extension.swift in Sources */, 6EA425A52218E6AE00B074B5 /* (null) in Sources */, @@ -4520,6 +4960,7 @@ 6E9B11B822C5489600C22D81 /* OTUtils.swift in Sources */, 6E9B119022C5488300C22D81 /* AttributeValueTests.swift in Sources */, 6E994B4025A3E6EA00999262 /* DecisionResponse.swift in Sources */, + 84E2E9802855875E001114AB /* OdpEventManager.swift in Sources */, 6E75175822C520D400B2B157 /* LogMessage.swift in Sources */, 6E9B119422C5488300C22D81 /* VariableTests.swift in Sources */, 6E9B11A222C5488300C22D81 /* ConditionHolderTests.swift in Sources */, @@ -4536,6 +4977,8 @@ 6E7518CE22C520D400B2B157 /* Audience.swift in Sources */, 6E75189222C520D400B2B157 /* Project.swift in Sources */, 6E7516F822C520D400B2B157 /* OptimizelyError.swift in Sources */, + 84B4D75E27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 848617E82863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E424C09263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75189E22C520D400B2B157 /* Experiment.swift in Sources */, 6E75178822C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4548,6 +4991,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 84F6BAB327FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift in Sources */, 6E7E9B372523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift in Sources */, 6E9B115622C5486E00C22D81 /* DecisionListenerTests.swift in Sources */, 6E9B114722C5486E00C22D81 /* OptimizelyErrorTests.swift in Sources */, @@ -4562,14 +5006,19 @@ 6E86CEA524FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E7518A322C520D400B2B157 /* FeatureFlag.swift in Sources */, 0B97DD9E249D4A22003DE606 /* SemanticVersion.swift in Sources */, + 84B4D75527E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6ECB60CD234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B115222C5486E00C22D81 /* BucketTests_ExpToVariation.swift in Sources */, 6E7E9C2F25240D2E009E4426 /* OptimizelyUserContextTests_Objc.m in Sources */, 6E75189722C520D400B2B157 /* Experiment.swift in Sources */, + 84958C5E280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift in Sources */, 6E7516C122C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75178122C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, + 84861800286CF33700B7F41B /* OdpEvent.swift in Sources */, 6EC6DD4524ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E7517CB22C520D400B2B157 /* DefaultBucketer.swift in Sources */, + 845945C1287758A000D13E11 /* OdpConfig.swift in Sources */, + 8464087528130D3200CCF97D /* Integration.swift in Sources */, 6E5D12202638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E9B115022C5486E00C22D81 /* BucketTests_Base.swift in Sources */, 6E9B114522C5486E00C22D81 /* MurmurTests.swift in Sources */, @@ -4586,32 +5035,38 @@ 6E7516D922C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E7516E522C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7E9B552523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */, + 6E652305278E688B00954EA1 /* LruCache.swift in Sources */, 6EC6DD3524ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E20050926B4D28500278087 /* MockLogger.swift in Sources */, 6E75175122C520D400B2B157 /* LogMessage.swift in Sources */, 6E75184F22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E75190F22C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75193322C520D500B2B157 /* OPTDataStore.swift in Sources */, + 84861811286D0B8900B7F41B /* OdpSegmentManagerTests.swift in Sources */, 6E7517EF22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E75194B22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75195722C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E75181322C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD6924AE94820017D296 /* OptimizelyUserContextTests.swift in Sources */, 6E75171522C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, + 6E6522E3278E4F3800954EA1 /* OdpManager.swift in Sources */, 6EA2CC272345618E001E7531 /* OptimizelyConfig.swift in Sources */, + 84861815286D0B8900B7F41B /* OdpVuidManagerTests.swift in Sources */, C78CAFA724486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E75185B22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E7516B522C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7516A922C520D400B2B157 /* DefaultLogger.swift in Sources */, - 6E9B114822C5486E00C22D81 /* OptimizelySwiftSDKiOSTests.swift in Sources */, 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, + 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift in Sources */, + 84E2E9472852A378001114AB /* OdpVuidManager.swift in Sources */, 6E623F06253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E424BE2263228E90081004A /* AtomicArray.swift in Sources */, 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, 6E9B114F22C5486E00C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E7518F722C520D500B2B157 /* UserAttribute.swift in Sources */, 6E75174522C520D400B2B157 /* HandlerRegistryService.swift in Sources */, + 8486181B286D188B00B7F41B /* OdpSegmentApiManagerTests.swift in Sources */, 6E8A3D492637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E981FC2232C363300FADDD6 /* DecisionListenerTests_Datafile.swift in Sources */, 6E9B114A22C5486E00C22D81 /* BucketTests_Others.swift in Sources */, @@ -4621,11 +5076,14 @@ 6E7518D322C520D400B2B157 /* AttributeValue.swift in Sources */, 6E0A72D426C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift in Sources */, 6EF41A332522BE1900EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */, + 848617EF2863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E27ECBE266FD78600B4A6D4 /* DecisionReasonsTests.swift in Sources */, 6E9B115E22C5486E00C22D81 /* DataStoreTests.swift in Sources */, 6E4544AF270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75177522C520D400B2B157 /* SDKVersion.swift in Sources */, + 848617CD2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75180722C520D400B2B157 /* DataStoreFile.swift in Sources */, + 8486181D286D188B00B7F41B /* OdpEventApiManagerTests.swift in Sources */, 6E75183722C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B115D22C5486E00C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */, 6E75173922C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -4643,6 +5101,8 @@ 6E9B115722C5486E00C22D81 /* DecisionServiceTests_Features.swift in Sources */, 6E9B115522C5486E00C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3825A3E6EA00999262 /* DecisionResponse.swift in Sources */, + 848617DF2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, + 84861813286D0B8900B7F41B /* OdpManagerTests.swift in Sources */, 6E7516FD22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187322C520D400B2B157 /* Variation.swift in Sources */, 6E7517E322C520D400B2B157 /* DefaultDecisionService.swift in Sources */, @@ -4655,12 +5115,17 @@ 6E7518BB22C520D400B2B157 /* Variable.swift in Sources */, 6E7518AF22C520D400B2B157 /* Group.swift in Sources */, 6EF8DE3524BF7D69008B9488 /* DecisionReasons.swift in Sources */, + 84E2E96628540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, + 84861817286D0B8900B7F41B /* OdpEventManagerTests.swift in Sources */, 6E7517A522C520D400B2B157 /* Array+Extension.swift in Sources */, 6E9B115122C5486E00C22D81 /* NotificationCenterTests.swift in Sources */, 6E75184322C520D400B2B157 /* Event.swift in Sources */, + 84644AB328F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift in Sources */, 6E75193F22C520D500B2B157 /* OPTDecisionService.swift in Sources */, 84E7ABC027D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E7516CD22C520D400B2B157 /* OPTLogger.swift in Sources */, + 8428D3D02807337400D0FB0C /* LruCacheTests.swift in Sources */, + 84E2E9772855875E001114AB /* OdpEventManager.swift in Sources */, 6E7517FB22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4681,17 +5146,21 @@ 6E7516FF22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187522C520D400B2B157 /* Variation.swift in Sources */, 0B97DD99249D332C003DE606 /* SemanticVersionTests.swift in Sources */, + 6E652309278E688B00954EA1 /* LruCache.swift in Sources */, 6E9B118622C5488100C22D81 /* ProjectConfigTests.swift in Sources */, 6E7518F922C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7517F122C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B117D22C5488100C22D81 /* AttributeTests.swift in Sources */, + 845945C4287758A100D13E11 /* OdpConfig.swift in Sources */, 6EC6DD3824ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E9B118B22C5488100C22D81 /* EventTests.swift in Sources */, 6E75188122C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E9B117F22C5488100C22D81 /* FeatureFlagTests.swift in Sources */, 6E75190522C520D500B2B157 /* Attribute.swift in Sources */, + 84861804286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75170B22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C322C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 84E2E94B2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75194122C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185122C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20050D26B4D28500278087 /* MockLogger.swift in Sources */, @@ -4710,21 +5179,25 @@ 6E7516CF22C520D400B2B157 /* OPTLogger.swift in Sources */, 6E75182D22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7518D522C520D400B2B157 /* AttributeValue.swift in Sources */, + 84640881281320F000CCF97D /* IntegrationTests.swift in Sources */, 6E9B118222C5488100C22D81 /* AudienceTests_Evaluate.swift in Sources */, 6E75191D22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75176B22C520D400B2B157 /* Utils.swift in Sources */, 6E7516DB22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E34A61C2319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B118722C5488100C22D81 /* UserAttributeTests.swift in Sources */, + 848617F32863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E5D12242638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75183922C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B118A22C5488100C22D81 /* ExperimentTests.swift in Sources */, 6E7516E722C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75181522C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE2124BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 8464087928130D3200CCF97D /* Integration.swift in Sources */, 6E9B118122C5488100C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184522C520D400B2B157 /* Event.swift in Sources */, 6E75191122C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, + 848617D12863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E9B118F22C5488100C22D81 /* ConditionHolderTests_Evaluate.swift in Sources */, 84E7ABC427D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E9B117B22C5488100C22D81 /* EventForDispatchTests.swift in Sources */, @@ -4738,12 +5211,14 @@ 6E7518E122C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75178F22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11AD22C5489300C22D81 /* MockUrlSession.swift in Sources */, + 6E6522E7278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E9B117C22C5488100C22D81 /* FeatureVariableTests.swift in Sources */, 6E75175F22C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF5E2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516B722C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E623F09253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E4544B3270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 84E2E96A28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75179B22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517A722C520D400B2B157 /* Array+Extension.swift in Sources */, 6EA425962218E6AD00B074B5 /* (null) in Sources */, @@ -4752,6 +5227,7 @@ 6E9B11AE22C5489300C22D81 /* OTUtils.swift in Sources */, 6E9B117A22C5488100C22D81 /* AttributeValueTests.swift in Sources */, 6E994B3B25A3E6EA00999262 /* DecisionResponse.swift in Sources */, + 84E2E97B2855875E001114AB /* OdpEventManager.swift in Sources */, 6E75175322C520D400B2B157 /* LogMessage.swift in Sources */, 6E9B117E22C5488100C22D81 /* VariableTests.swift in Sources */, 6E9B118C22C5488100C22D81 /* ConditionHolderTests.swift in Sources */, @@ -4768,6 +5244,8 @@ 6E7518C922C520D400B2B157 /* Audience.swift in Sources */, 6E75188D22C520D400B2B157 /* Project.swift in Sources */, 6E7516F322C520D400B2B157 /* OptimizelyError.swift in Sources */, + 84B4D75927E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 848617E32863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E424C04263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75189922C520D400B2B157 /* Experiment.swift in Sources */, 6E75178322C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4785,11 +5263,13 @@ 6E75176C22C520D400B2B157 /* Utils.swift in Sources */, 6E7516C422C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, C78CAFAB24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, + 84E2E96B28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E86CEAA24FDC848005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75173022C520D400B2B157 /* Constants.swift in Sources */, 6E75181622C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75188E22C520D400B2B157 /* Project.swift in Sources */, 6E994B3C25A3E6EA00999262 /* DecisionResponse.swift in Sources */, + 6E6522E8278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E75189A22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3924ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E75179C22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -4801,14 +5281,17 @@ 6EC6DD4924ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75188222C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E5D12252638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, + 848617F42863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6ECB60D0234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B11E322C548AF00C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176022C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75192A22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, + 84B4D75A27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E7517DA22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517E622C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171822C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174822C520D400B2B157 /* HandlerRegistryService.swift in Sources */, + 84E2E94C2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E7518FA22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7516E822C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75191222C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, @@ -4816,7 +5299,9 @@ 6E75182E22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7516DC22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E75182222C520D400B2B157 /* BatchEventBuilder.swift in Sources */, + 84861805286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75190622C520D500B2B157 /* Attribute.swift in Sources */, + 848617D22863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75183A22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6EA2CC2A2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E9B11B022C5489400C22D81 /* OTUtils.swift in Sources */, @@ -4832,14 +5317,17 @@ 6E75170022C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E8A3D4E2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E9B11E222C548AF00C22D81 /* OtherTests.swift in Sources */, + 8464087A28130D3200CCF97D /* Integration.swift in Sources */, 6E75185E22C520D400B2B157 /* FeatureVariable.swift in Sources */, 84E7ABC527D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E7518BE22C520D400B2B157 /* Variable.swift in Sources */, 6E7518CA22C520D400B2B157 /* Audience.swift in Sources */, + 848617E42863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E75187622C520D400B2B157 /* Variation.swift in Sources */, 6E7517F222C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B11AF22C5489400C22D81 /* MockUrlSession.swift in Sources */, 6E7516F422C520D400B2B157 /* OptimizelyError.swift in Sources */, + 845945C5287758A200D13E11 /* OdpConfig.swift in Sources */, 6E75195A22C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E75177822C520D400B2B157 /* SDKVersion.swift in Sources */, 6E75194E22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -4848,6 +5336,7 @@ 6E7518D622C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7518A622C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75186A22C520D400B2B157 /* Rollout.swift in Sources */, + 84E2E97C2855875E001114AB /* OdpEventManager.swift in Sources */, 6E75178422C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EF8DE1424BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75175422C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4857,6 +5346,7 @@ 6E7517A822C520D400B2B157 /* Array+Extension.swift in Sources */, 6E7518EE22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E75185222C520D400B2B157 /* ProjectConfig.swift in Sources */, + 6E65230A278E688B00954EA1 /* LruCache.swift in Sources */, C78CAF5F2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E623F0A253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75179022C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, @@ -4874,11 +5364,13 @@ 6E75177122C520D400B2B157 /* Utils.swift in Sources */, 6E7516C922C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, C78CAFB024486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, + 84E2E97028540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E86CEAF24FDC84B005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75173522C520D400B2B157 /* Constants.swift in Sources */, 6E75181B22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75189322C520D400B2B157 /* Project.swift in Sources */, 6E994B4125A3E6EA00999262 /* DecisionResponse.swift in Sources */, + 6E6522ED278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E75189F22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3E24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E7517A122C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -4890,14 +5382,17 @@ 6EC6DD4E24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75188722C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E5D122A2638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, + 848617F92863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6ECB60D5234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B11E522C548B100C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176522C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75192F22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, + 84B4D75F27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E7517DF22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517EB22C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171D22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174D22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, + 84E2E9512852A378001114AB /* OdpVuidManager.swift in Sources */, 6E7518FF22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7516ED22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75191722C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, @@ -4905,7 +5400,9 @@ 6E75183322C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7516E122C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E75182722C520D400B2B157 /* BatchEventBuilder.swift in Sources */, + 8486180A286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75190B22C520D500B2B157 /* Attribute.swift in Sources */, + 848617D72863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75183F22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6EA2CC2F2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E9B11BA22C5489700C22D81 /* OTUtils.swift in Sources */, @@ -4921,14 +5418,17 @@ 6E75170522C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E8A3D532637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E9B11E422C548B100C22D81 /* OtherTests.swift in Sources */, + 8464087F28130D3200CCF97D /* Integration.swift in Sources */, 6E75186322C520D400B2B157 /* FeatureVariable.swift in Sources */, 84E7ABCA27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E7518C322C520D400B2B157 /* Variable.swift in Sources */, 6E7518CF22C520D400B2B157 /* Audience.swift in Sources */, + 848617E92863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E75187B22C520D400B2B157 /* Variation.swift in Sources */, 6E7517F722C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B11B922C5489700C22D81 /* MockUrlSession.swift in Sources */, 6E7516F922C520D400B2B157 /* OptimizelyError.swift in Sources */, + 845945CA287758A700D13E11 /* OdpConfig.swift in Sources */, 6E75195F22C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E75177D22C520D400B2B157 /* SDKVersion.swift in Sources */, 6E75195322C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -4937,6 +5437,7 @@ 6E7518DB22C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7518AB22C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75186F22C520D400B2B157 /* Rollout.swift in Sources */, + 84E2E9812855875E001114AB /* OdpEventManager.swift in Sources */, 6E75178922C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EF8DE1924BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75175922C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4946,6 +5447,7 @@ 6E7517AD22C520D400B2B157 /* Array+Extension.swift in Sources */, 6E7518F322C520D500B2B157 /* ConditionHolder.swift in Sources */, 6E75185722C520D400B2B157 /* ProjectConfig.swift in Sources */, + 6E65230F278E688B00954EA1 /* LruCache.swift in Sources */, C78CAF642445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E623F0F253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75179522C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, @@ -4963,13 +5465,16 @@ 6E75190022C520D500B2B157 /* Attribute.swift in Sources */, 6E7518AC22C520D400B2B157 /* Group.swift in Sources */, 6E7517C822C520D400B2B157 /* DefaultBucketer.swift in Sources */, + 84E2E9722855875E001114AB /* OdpEventManager.swift in Sources */, 6EF8DE0C24BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6EF8DE3124BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E7517BC22C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E7516CA22C520D400B2B157 /* OPTLogger.swift in Sources */, 84E7ABBB27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E75182822C520D400B2B157 /* BatchEvent.swift in Sources */, + 8464087028130D3200CCF97D /* Integration.swift in Sources */, 6E623F02253F9045000617D0 /* DecisionInfo.swift in Sources */, + 845945BC2877589D00D13E11 /* OdpConfig.swift in Sources */, 6E75184022C520D400B2B157 /* Event.swift in Sources */, 6E7516E222C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7517D422C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, @@ -4984,7 +5489,9 @@ 6E75180422C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E86CEA224FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75195422C520D500B2B157 /* OPTBucketer.swift in Sources */, + 848617DA2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E75171E22C520D400B2B157 /* OptimizelyResult.swift in Sources */, + 84E2E9422852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75189422C520D400B2B157 /* Experiment.swift in Sources */, @@ -4998,9 +5505,11 @@ 6E7517F822C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E4544AA270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E7517A222C520D400B2B157 /* Array+Extension.swift in Sources */, + 848617EA2863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E75194822C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7518E822C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E424BDD263228E90081004A /* AtomicArray.swift in Sources */, + 848617FB286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75191822C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E7518B822C520D400B2B157 /* Variable.swift in Sources */, 6E75173622C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5009,6 +5518,7 @@ 6E75186422C520D400B2B157 /* Rollout.swift in Sources */, 6E75179622C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, C78CAF582445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, + 848617C82863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75170622C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7518A022C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75174222C520D400B2B157 /* HandlerRegistryService.swift in Sources */, @@ -5016,6 +5526,7 @@ 6E75184C22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E75181022C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD4124ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, + 6E652300278E688B00954EA1 /* LruCache.swift in Sources */, 6E7516EE22C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75183422C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75171222C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -5023,9 +5534,12 @@ 6E7516B222C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E75192422C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */, + 84B4D75027E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 6E6522DE278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E75193022C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7517E022C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E7516FA22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, + 84E2E96128540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75187C22C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E7517EC22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E75174E22C520D400B2B157 /* LogMessage.swift in Sources */, @@ -5042,15 +5556,20 @@ files = ( 6E7516FC22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E7516B422C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 845945C02877589F00D13E11 /* OdpConfig.swift in Sources */, 6E75175022C520D400B2B157 /* LogMessage.swift in Sources */, + 848617EE2863E21200B7F41B /* OdpEventApiManager.swift in Sources */, 6E75193222C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75190E22C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75172022C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75172C22C520D400B2B157 /* Constants.swift in Sources */, + 848617CC2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75184222C520D400B2B157 /* Event.swift in Sources */, + 84E2E96528540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75170822C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177422C520D400B2B157 /* SDKVersion.swift in Sources */, 6E7516C022C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 848617FF286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75189622C520D400B2B157 /* Experiment.swift in Sources */, 84E7ABBF27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E75175C22C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -5078,12 +5597,14 @@ C78CAFA624486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7518D222C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516A822C520D400B2B157 /* DefaultLogger.swift in Sources */, + 6E6522E2278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E623F05253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5A2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E75194A22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E86CEA724FDC846005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E5D121F2638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E7516CC22C520D400B2B157 /* OPTLogger.swift in Sources */, + 84B4D75427E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E75185A22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6EC6DD3424ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E7517E222C520D400B2B157 /* DefaultDecisionService.swift in Sources */, @@ -5093,21 +5614,25 @@ 6E7518DE22C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6EF8DE1D24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7518EA22C520D400B2B157 /* ConditionHolder.swift in Sources */, + 84E2E9462852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75182A22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75191A22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E20050826B4D28500278087 /* MockLogger.swift in Sources */, 6E7518AE22C520D400B2B157 /* Group.swift in Sources */, 6E7518C622C520D400B2B157 /* Audience.swift in Sources */, 6E75176822C520D400B2B157 /* Utils.swift in Sources */, + 848617DE2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E75181E22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E75174422C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E7516F022C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75187E22C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6EA2CC262345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E7517CA22C520D400B2B157 /* DefaultBucketer.swift in Sources */, + 8464087428130D3200CCF97D /* Integration.swift in Sources */, 6E7517FA22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 0B97DD9D249D4A22003DE606 /* SemanticVersion.swift in Sources */, 6E9B11A722C5489200C22D81 /* MockUrlSession.swift in Sources */, + 84E2E9762855875E001114AB /* OdpEventManager.swift in Sources */, 6E7517D622C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75178022C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E75171422C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -5121,6 +5646,7 @@ 6E75193E22C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75178C22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E4544AE270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 6E652304278E688B00954EA1 /* LruCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5128,6 +5654,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8464087328130D3200CCF97D /* Integration.swift in Sources */, 75C71A0025E454460084187E /* OptimizelyError.swift in Sources */, 75C71A0125E454460084187E /* OptimizelyLogLevel.swift in Sources */, 75C71A0225E454460084187E /* OptimizelyClient.swift in Sources */, @@ -5138,6 +5665,7 @@ 75C71C3925E45A2B0084187E /* WatchBackgroundNotifier.swift in Sources */, 75C71A0725E454460084187E /* OptimizelyJSON.swift in Sources */, 75C71A0825E454460084187E /* OptimizelyJSON+ObjC.swift in Sources */, + 84B4D75327E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 75C71A0925E454460084187E /* OptimizelyClient+Decide.swift in Sources */, 75C71A0A25E454460084187E /* OptimizelyUserContext.swift in Sources */, 75C71A0B25E454460084187E /* OptimizelyDecideOption.swift in Sources */, @@ -5148,10 +5676,13 @@ 75C71A0F25E454460084187E /* DefaultUserProfileService.swift in Sources */, 75C71A1025E454460084187E /* DefaultEventDispatcher.swift in Sources */, 75C71A1125E454460084187E /* OPTLogger.swift in Sources */, + 848617CB2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 75C71A1225E454460084187E /* OPTUserProfileService.swift in Sources */, 75C71A1325E454460084187E /* OPTEventDispatcher.swift in Sources */, 75C71A1425E454460084187E /* DefaultDatafileHandler.swift in Sources */, 6E424BE0263228E90081004A /* AtomicArray.swift in Sources */, + 84E2E9752855875E001114AB /* OdpEventManager.swift in Sources */, + 6E652303278E688B00954EA1 /* LruCache.swift in Sources */, 75C71A1525E454460084187E /* DecisionInfo.swift in Sources */, 75C71A1625E454460084187E /* DefaultBucketer.swift in Sources */, 75C71A1725E454460084187E /* DefaultNotificationCenter.swift in Sources */, @@ -5159,6 +5690,7 @@ 75C71A1925E454460084187E /* DecisionReasons.swift in Sources */, 75C71A1A25E454460084187E /* DecisionResponse.swift in Sources */, 75C71A1B25E454460084187E /* DataStoreMemory.swift in Sources */, + 84E2E96428540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 75C71A1C25E454460084187E /* DataStoreUserDefaults.swift in Sources */, 75C71A1D25E454460084187E /* DataStoreFile.swift in Sources */, 75C71A1E25E454460084187E /* DataStoreQueueStackImpl.swift in Sources */, @@ -5167,6 +5699,8 @@ 75C71A2125E454460084187E /* EventForDispatch.swift in Sources */, 75C71A2225E454460084187E /* SemanticVersion.swift in Sources */, 75C71A2325E454460084187E /* Audience.swift in Sources */, + 84E2E9452852A378001114AB /* OdpVuidManager.swift in Sources */, + 6E6522E1278E4F3800954EA1 /* OdpManager.swift in Sources */, 75C71A2425E454460084187E /* AttributeValue.swift in Sources */, 75C71A2525E454460084187E /* ConditionLeaf.swift in Sources */, 75C71A2625E454460084187E /* ConditionHolder.swift in Sources */, @@ -5183,6 +5717,7 @@ 75C71A3025E454460084187E /* FeatureFlag.swift in Sources */, 75C71A3125E454460084187E /* Group.swift in Sources */, 75C71A3225E454460084187E /* Variable.swift in Sources */, + 848617DD2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 75C71A3325E454460084187E /* Attribute.swift in Sources */, 75C71A3425E454460084187E /* BackgroundingCallbacks.swift in Sources */, 75C71A3525E454460084187E /* OPTNotificationCenter.swift in Sources */, @@ -5194,10 +5729,13 @@ 75C71A3B25E454460084187E /* ArrayEventForDispatch+Extension.swift in Sources */, 75C71A3C25E454460084187E /* OptimizelyClient+Extension.swift in Sources */, 75C71A3D25E454460084187E /* DataStoreQueueStackImpl+Extension.swift in Sources */, + 845945BF2877589F00D13E11 /* OdpConfig.swift in Sources */, 75C71A3E25E454460084187E /* Array+Extension.swift in Sources */, 75C71A3F25E454460084187E /* Constants.swift in Sources */, 75C71A4025E454460084187E /* Notifications.swift in Sources */, 75C71A4125E454460084187E /* MurmurHash3.swift in Sources */, + 848617ED2863E21200B7F41B /* OdpEventApiManager.swift in Sources */, + 848617FE286CF33700B7F41B /* OdpEvent.swift in Sources */, 75C71A4225E454460084187E /* HandlerRegistryService.swift in Sources */, 75C71A4325E454460084187E /* LogMessage.swift in Sources */, 75C71A4425E454460084187E /* AtomicProperty.swift in Sources */, @@ -5216,13 +5754,16 @@ BD6485402491474500F30986 /* Attribute.swift in Sources */, BD6485412491474500F30986 /* Group.swift in Sources */, BD6485422491474500F30986 /* DefaultBucketer.swift in Sources */, + 84E2E9742855875E001114AB /* OdpEventManager.swift in Sources */, 6EF8DE0E24BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6EF8DE3324BF7D69008B9488 /* DecisionReasons.swift in Sources */, BD6485432491474500F30986 /* DefaultDatafileHandler.swift in Sources */, BD6485442491474500F30986 /* OPTLogger.swift in Sources */, 84E7ABBD27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, BD6485452491474500F30986 /* BatchEvent.swift in Sources */, + 8464087228130D3200CCF97D /* Integration.swift in Sources */, 6E623F04253F9045000617D0 /* DecisionInfo.swift in Sources */, + 845945BE2877589E00D13E11 /* OdpConfig.swift in Sources */, BD6485462491474500F30986 /* Event.swift in Sources */, BD6485472491474500F30986 /* OPTEventDispatcher.swift in Sources */, BD6485482491474500F30986 /* DefaultNotificationCenter.swift in Sources */, @@ -5237,7 +5778,9 @@ BD64854F2491474500F30986 /* DataStoreFile.swift in Sources */, 6E86CEA424FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, BD6485502491474500F30986 /* OPTBucketer.swift in Sources */, + 848617DC2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, BD6485512491474500F30986 /* OptimizelyResult.swift in Sources */, + 84E2E9442852A378001114AB /* OdpVuidManager.swift in Sources */, BD6485522491474500F30986 /* Constants.swift in Sources */, BD6485532491474500F30986 /* DefaultLogger.swift in Sources */, BD6485542491474500F30986 /* Experiment.swift in Sources */, @@ -5251,9 +5794,11 @@ BD64855C2491474500F30986 /* DataStoreUserDefaults.swift in Sources */, 6E4544AC270E67C800F2CEBC /* NetworkReachability.swift in Sources */, BD64855D2491474500F30986 /* Array+Extension.swift in Sources */, + 848617EC2863E21200B7F41B /* OdpEventApiManager.swift in Sources */, BD64855E2491474500F30986 /* OPTDatafileHandler.swift in Sources */, BD64855F2491474500F30986 /* ConditionHolder.swift in Sources */, 6E424BDF263228E90081004A /* AtomicArray.swift in Sources */, + 848617FD286CF33700B7F41B /* OdpEvent.swift in Sources */, BD6485602491474500F30986 /* OPTNotificationCenter.swift in Sources */, BD6485612491474500F30986 /* Variable.swift in Sources */, BD6485622491474500F30986 /* MurmurHash3.swift in Sources */, @@ -5262,6 +5807,7 @@ BD6485642491474500F30986 /* Rollout.swift in Sources */, BD6485652491474500F30986 /* DataStoreQueueStackImpl+Extension.swift in Sources */, BD6485662491474500F30986 /* OptimizelyJSON.swift in Sources */, + 848617CA2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, BD6485672491474500F30986 /* OptimizelyClient.swift in Sources */, BD6485682491474500F30986 /* FeatureFlag.swift in Sources */, BD6485692491474500F30986 /* HandlerRegistryService.swift in Sources */, @@ -5269,6 +5815,7 @@ BD64856B2491474500F30986 /* ProjectConfig.swift in Sources */, BD64856C2491474500F30986 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD4324ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, + 6E652302278E688B00954EA1 /* LruCache.swift in Sources */, BD64856D2491474500F30986 /* OptimizelyError.swift in Sources */, BD64856E2491474500F30986 /* EventForDispatch.swift in Sources */, BD64856F2491474500F30986 /* OptimizelyClient+ObjC.swift in Sources */, @@ -5276,9 +5823,12 @@ BD6485712491474500F30986 /* DefaultUserProfileService.swift in Sources */, BD6485722491474500F30986 /* DataStoreQueueStack.swift in Sources */, BD6485732491474500F30986 /* OptimizelyConfig.swift in Sources */, + 84B4D75227E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 6E6522E0278E4F3800954EA1 /* OdpManager.swift in Sources */, BD6485742491474500F30986 /* OPTDataStore.swift in Sources */, BD6485752491474500F30986 /* DefaultDecisionService.swift in Sources */, BD6485762491474500F30986 /* OptimizelyLogLevel.swift in Sources */, + 84E2E96328540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, BD6485772491474500F30986 /* TrafficAllocation.swift in Sources */, BD6485782491474500F30986 /* DataStoreMemory.swift in Sources */, BD6485792491474500F30986 /* LogMessage.swift in Sources */, diff --git a/OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist b/OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist index 24affa1b..ba601826 100644 --- a/OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist +++ b/OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -4,7 +4,7 @@ FILEHEADER -// Copyright 2021, Optimizely, Inc. and contributors +// Copyright 2022, Optimizely, Inc. and contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/Data Model/Audience/Audience.swift b/Sources/Data Model/Audience/Audience.swift index aafa5f92..252b6218 100644 --- a/Sources/Data Model/Audience/Audience.swift +++ b/Sources/Data Model/Audience/Audience.swift @@ -70,8 +70,36 @@ struct Audience: Codable, Equatable, OptimizelyAudience { try container.encode(conditionHolder, forKey: .conditions) } - func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool { - return try conditionHolder.evaluate(project: project, attributes: attributes) + func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool { + return try conditionHolder.evaluate(project: project, user: user) + } + + /// Extract all audience segments used in this audience conditions. + /// - Returns: a String array of segment names. + func getSegments() -> [String] { + let segments = getSegments(condition: conditionHolder) + return Array(Set(segments)) + } + + func getSegments(condition: ConditionHolder) -> [String] { + var segments = [String]() + + switch condition { + case .logicalOp: + return [] + case .leaf(let leaf): + if case .attribute(let userAttribute) = leaf { + if userAttribute.matchSupported == .qualified, let strValue = userAttribute.value?.stringValue { + segments.append(strValue) + } + } + case .array(let conditions): + conditions.forEach { + segments.append(contentsOf: getSegments(condition: $0)) + } + } + + return segments } } diff --git a/Sources/Data Model/Audience/ConditionHolder.swift b/Sources/Data Model/Audience/ConditionHolder.swift index 0f290b39..e2584e4c 100644 --- a/Sources/Data Model/Audience/ConditionHolder.swift +++ b/Sources/Data Model/Audience/ConditionHolder.swift @@ -61,14 +61,14 @@ enum ConditionHolder: Codable, Equatable { } } - func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool { + func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool { switch self { case .logicalOp: throw OptimizelyError.conditionInvalidFormat("Logical operation not evaluated") case .leaf(let conditionLeaf): - return try conditionLeaf.evaluate(project: project, attributes: attributes) + return try conditionLeaf.evaluate(project: project, user: user) case .array(let conditions): - return try conditions.evaluate(project: project, attributes: attributes) + return try conditions.evaluate(project: project, user: user) } } @@ -111,24 +111,24 @@ extension ConditionHolder { extension Array where Element == ConditionHolder { - func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool { + func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool { guard let firstItem = self.first else { throw OptimizelyError.conditionInvalidFormat("Empty condition array") } switch firstItem { case .logicalOp(let op): - return try evaluate(op: op, project: project, attributes: attributes) + return try evaluate(op: op, project: project, user: user) case .leaf: // special case - no logical operator // implicit or - return try [[ConditionHolder.logicalOp(.or)], self].flatMap({$0}).evaluate(op: LogicalOp.or, project: project, attributes: attributes) + return try [[ConditionHolder.logicalOp(.or)], self].flatMap({$0}).evaluate(op: LogicalOp.or, project: project, user: user) default: throw OptimizelyError.conditionInvalidFormat("Invalid first item") } } - func evaluate(op: LogicalOp, project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool { + func evaluate(op: LogicalOp, project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool { guard self.count > 0 else { throw OptimizelyError.conditionInvalidFormat("Empty condition array") } @@ -138,7 +138,7 @@ extension Array where Element == ConditionHolder { // create closure array for delayed evaluations to avoid unnecessary ops let evalList = itemsAfterOpTrimmed.map { holder -> ThrowableCondition in return { - return try holder.evaluate(project: project, attributes: attributes) + return try holder.evaluate(project: project, user: user) } } diff --git a/Sources/Data Model/Audience/ConditionLeaf.swift b/Sources/Data Model/Audience/ConditionLeaf.swift index b51c5787..7829b0a1 100644 --- a/Sources/Data Model/Audience/ConditionLeaf.swift +++ b/Sources/Data Model/Audience/ConditionLeaf.swift @@ -45,16 +45,16 @@ enum ConditionLeaf: Codable, Equatable { } } - func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool { + func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool { switch self { case .audienceId(let id): guard let project = project else { throw OptimizelyError.conditionCannotBeEvaluated("audienceId: \(id)") } - return try project.evaluateAudience(audienceId: id, attributes: attributes) + return try project.evaluateAudience(audienceId: id, user: user) case .attribute(let userAttribute): - return try userAttribute.evaluate(attributes: attributes) + return try userAttribute.evaluate(user: user) } } diff --git a/Sources/Data Model/Audience/UserAttribute.swift b/Sources/Data Model/Audience/UserAttribute.swift index bcf4c642..bbb1d476 100644 --- a/Sources/Data Model/Audience/UserAttribute.swift +++ b/Sources/Data Model/Audience/UserAttribute.swift @@ -37,6 +37,7 @@ struct UserAttribute: Codable, Equatable { enum ConditionType: String, Codable { case customAttribute = "custom_attribute" + case thirdPartyDimension = "third_party_dimension" } enum ConditionMatch: String, Codable { @@ -52,6 +53,7 @@ struct UserAttribute: Codable, Equatable { case semver_le case semver_gt case semver_ge + case qualified } var typeSupported: ConditionType? { @@ -98,7 +100,7 @@ struct UserAttribute: Codable, Equatable { extension UserAttribute { - func evaluate(attributes: OptimizelyAttributes?) throws -> Bool { + func evaluate(user: OptimizelyUserContext) throws -> Bool { // invalid type - parsed for forward compatibility only (but evaluation fails) if typeSupported == nil { @@ -114,63 +116,77 @@ extension UserAttribute { throw OptimizelyError.userAttributeInvalidName(stringRepresentation) } - let attributes = attributes ?? OptimizelyAttributes() - - let rawAttributeValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'" + let attributes = user.attributes + let rawValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'" - if matchFinal != .exists { - if !attributes.keys.contains(nameFinal) { - throw OptimizelyError.missingAttributeValue(stringRepresentation, nameFinal) - } + if matchFinal == .exists { + return !(rawValue is NSNull || rawValue == nil) + } + + // all other matches requires valid value - if value == nil { - throw OptimizelyError.userAttributeNilValue(stringRepresentation) - } + guard let value = value else { + throw OptimizelyError.userAttributeNilValue(stringRepresentation) + } - if rawAttributeValue == nil { - throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal) + if matchFinal == .qualified { + // NOTE: name ("odp.audiences") and type("third_party_dimension") not used + + guard case .string(let strValue) = value else { + throw OptimizelyError.evaluateAttributeInvalidCondition(stringRepresentation) } + return user.isQualifiedFor(segment: strValue) + } + + // all other matches requires attribute value + + guard attributes.keys.contains(nameFinal) else { + throw OptimizelyError.missingAttributeValue(stringRepresentation, nameFinal) } + guard let rawAttributeValue = rawValue else { + throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal) + } + switch matchFinal { - case .exists: - return !(rawAttributeValue is NSNull || rawAttributeValue == nil) case .exact: - return try value!.isExactMatch(with: rawAttributeValue!, condition: stringRepresentation, name: nameFinal) + return try value.isExactMatch(with: rawAttributeValue, condition: stringRepresentation, name: nameFinal) case .substring: - return try value!.isSubstring(of: rawAttributeValue!, condition: stringRepresentation, name: nameFinal) + return try value.isSubstring(of: rawAttributeValue, condition: stringRepresentation, name: nameFinal) case .lt: // user attribute "less than" this condition value // so evaluate if this condition value "isGreater" than the user attribute value - return try value!.isGreater(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal) + return try value.isGreater(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal) case .le: // user attribute "less than" or equal this condition value // so evaluate if this condition value "isGreater" than or equal the user attribute value - return try value!.isGreaterOrEqual(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal) + return try value.isGreaterOrEqual(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal) case .gt: // user attribute "greater than" this condition value // so evaluate if this condition value "isLess" than the user attribute value - return try value!.isLess(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal) + return try value.isLess(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal) case .ge: // user attribute "greater than or equal" this condition value // so evaluate if this condition value "isLess" than or equal the user attribute value - return try value!.isLessOrEqual(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal) + return try value.isLessOrEqual(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal) // semantic versioning seems unique. the comarison is to compare verion but the passed in version is the target version. case .semver_eq: let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal) - return try targetValue.isSemanticVersionEqual(than: value!.stringValue) + return try targetValue.isSemanticVersionEqual(than: value.stringValue) case .semver_lt: let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal) - return try targetValue.isSemanticVersionLess(than: value!.stringValue) + return try targetValue.isSemanticVersionLess(than: value.stringValue) case .semver_le: let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal) - return try targetValue.isSemanticVersionLessOrEqual(than: value!.stringValue) + return try targetValue.isSemanticVersionLessOrEqual(than: value.stringValue) case .semver_gt: let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal) - return try targetValue.isSemanticVersionGreater(than: value!.stringValue) + return try targetValue.isSemanticVersionGreater(than: value.stringValue) case .semver_ge: let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal) - return try targetValue.isSemanticVersionGreaterOrEqual(than: value!.stringValue) + return try targetValue.isSemanticVersionGreaterOrEqual(than: value.stringValue) + default: + throw OptimizelyError.userAttributeInvalidMatch(stringRepresentation) } } diff --git a/Sources/Data Model/Integration.swift b/Sources/Data Model/Integration.swift new file mode 100644 index 00000000..ee56f03d --- /dev/null +++ b/Sources/Data Model/Integration.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct Integration: Codable, Equatable { + var key: String + var host: String? + var publicKey: String? +} diff --git a/Sources/Data Model/Project.swift b/Sources/Data Model/Project.swift index 0aabc96a..c85ad6a2 100644 --- a/Sources/Data Model/Project.swift +++ b/Sources/Data Model/Project.swift @@ -17,7 +17,7 @@ import Foundation protocol ProjectProtocol { - func evaluateAudience(audienceId: String, attributes: OptimizelyAttributes?) throws -> Bool + func evaluateAudience(audienceId: String, user: OptimizelyUserContext) throws -> Bool } // [REF]: datafile schema @@ -39,6 +39,7 @@ struct Project: Codable, Equatable { var anonymizeIP: Bool // V4 var rollouts: [Rollout] + var integrations: [Integration]? var typedAudiences: [Audience]? var featureFlags: [FeatureFlag] var botFiltering: Bool? @@ -55,7 +56,7 @@ struct Project: Codable, Equatable { // V3 case anonymizeIP // V4 - case rollouts, typedAudiences, featureFlags, botFiltering, sendFlagDecisions, sdkKey, environmentKey + case rollouts, integrations, typedAudiences, featureFlags, botFiltering, sendFlagDecisions, sdkKey, environmentKey } // Required since logger is not equatable @@ -63,14 +64,15 @@ struct Project: Codable, Equatable { return lhs.version == rhs.version && lhs.projectId == rhs.projectId && lhs.experiments == rhs.experiments && lhs.audiences == rhs.audiences && lhs.groups == rhs.groups && lhs.attributes == rhs.attributes && lhs.accountId == rhs.accountId && lhs.events == rhs.events && lhs.revision == rhs.revision && - lhs.anonymizeIP == rhs.anonymizeIP && lhs.rollouts == rhs.rollouts && lhs.typedAudiences == rhs.typedAudiences && + lhs.anonymizeIP == rhs.anonymizeIP && lhs.rollouts == rhs.rollouts && + lhs.integrations == rhs.integrations && lhs.typedAudiences == rhs.typedAudiences && lhs.featureFlags == rhs.featureFlags && lhs.botFiltering == rhs.botFiltering && lhs.sendFlagDecisions == rhs.sendFlagDecisions && lhs.sdkKey == rhs.sdkKey && lhs.environmentKey == rhs.environmentKey } } extension Project: ProjectProtocol { - func evaluateAudience(audienceId: String, attributes: OptimizelyAttributes?) throws -> Bool { + func evaluateAudience(audienceId: String, user: OptimizelyUserContext) throws -> Bool { guard let audience = getAudience(id: audienceId) else { throw OptimizelyError.conditionNoMatchingAudience(audienceId) } @@ -78,7 +80,7 @@ extension Project: ProjectProtocol { return LogMessage.audienceEvaluationStarted(audienceId, Utils.getConditionString(conditions: audience.conditionHolder)).description } - let result = try audience.evaluate(project: self, attributes: attributes) + let result = try audience.evaluate(project: self, user: user) logger.d(.audienceEvaluationResult(audienceId, result.description)) return result } diff --git a/Sources/Data Model/ProjectConfig.swift b/Sources/Data Model/ProjectConfig.swift index 44d7f42d..c1faa357 100644 --- a/Sources/Data Model/ProjectConfig.swift +++ b/Sources/Data Model/ProjectConfig.swift @@ -39,6 +39,7 @@ class ProjectConfig { var rolloutIdMap = [String: Rollout]() var allExperiments = [Experiment]() var flagVariationsMap = [String: [Variation]]() + var allSegments = [String]() // MARK: - Init @@ -54,7 +55,8 @@ class ProjectConfig { throw OptimizelyError.dataFileVersionInvalid(project.version) } - defer { self.project = project } // deferred-init will call "didSet" + self.project = project + updateProjectDependentProps() // project:didSet is not fired in init. explicitly called. } convenience init(datafile: String) throws { @@ -146,6 +148,11 @@ class ProjectConfig { return map }() + self.allSegments = { + let audiences = project.typedAudiences ?? [] + return Array(Set(audiences.flatMap { $0.getSegments() })) + }() + } func getAllRulesForFlag(_ flag: FeatureFlag) -> [Experiment] { @@ -200,6 +207,20 @@ extension ProjectConfig { return project.sendFlagDecisions ?? false } + /** + * ODP API server publicKey. + */ + var publicKeyForODP: String? { + return project.integrations?.filter { $0.key == "odp" }.first?.publicKey + } + + /** + * ODP API server host. + */ + var hostForODP: String? { + return project.integrations?.filter { $0.key == "odp" }.first?.host + } + /** * Get an Experiment object for a key. */ @@ -361,5 +382,4 @@ extension ProjectConfig { return nil } - } diff --git a/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index e8be66c2..20a38b10 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -56,6 +56,7 @@ extension OptimizelyClient { /// Set this to 0 to disable periodic downloading. /// - defaultLogLevel: default log level (optional. default = .info) /// - defaultDecisionOptions: default decision optiopns (optional) + /// - settings: SDK configuration (optional) public convenience init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, @@ -63,15 +64,17 @@ extension OptimizelyClient { userProfileService: OPTUserProfileService? = nil, periodicDownloadInterval: Int?, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil) { - + defaultDecideOptions: [OptimizelyDecideOption]? = nil, + settings: OptimizelySdkSettings? = nil) { + self.init(sdkKey: sdkKey, logger: logger, eventDispatcher: eventDispatcher, datafileHandler: datafileHandler, userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions) + defaultDecideOptions: defaultDecideOptions, + settings: settings) let interval = periodicDownloadInterval ?? 10 * 60 if interval > 0 { diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index ba40d7d8..276d3a15 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -102,8 +102,7 @@ class DefaultDecisionService: OPTDecisionService { // ---- check if the user passes audience targeting before bucketing ---- let audienceResponse = doesMeetAudienceConditions(config: config, experiment: experiment, - userId: userId, - attributes: attributes) + user: user) reasons.merge(audienceResponse.reasons) if audienceResponse.result ?? false { // bucket user into a variation @@ -138,8 +137,7 @@ class DefaultDecisionService: OPTDecisionService { func doesMeetAudienceConditions(config: ProjectConfig, experiment: Experiment, - userId: String, - attributes: OptimizelyAttributes, + user: OptimizelyUserContext, logType: Constants.EvaluationLogType = .experiment, loggingKey: String? = nil) -> DecisionResponse { let reasons = DecisionReasons() @@ -156,13 +154,13 @@ class DefaultDecisionService: OPTDecisionService { switch conditions { case .array(let arrConditions): if arrConditions.count > 0 { - result = try conditions.evaluate(project: config.project, attributes: attributes) + result = try conditions.evaluate(project: config.project, user: user) } else { // empty conditions (backward compatibility with "audienceIds" is ignored if exists even though empty result = true } case .leaf: - result = try conditions.evaluate(project: config.project, attributes: attributes) + result = try conditions.evaluate(project: config.project, user: user) default: result = true } @@ -177,7 +175,7 @@ class DefaultDecisionService: OPTDecisionService { logger.d { () -> String in return LogMessage.evaluatingAudiencesCombined(evType, finalLoggingKey, Utils.getConditionString(conditions: holder)).description } - result = try holder.evaluate(project: config.project, attributes: attributes) + result = try holder.evaluate(project: config.project, user: user) } } catch { if let error = error as? OptimizelyError { @@ -376,8 +374,7 @@ class DefaultDecisionService: OPTDecisionService { let audienceDecisionResponse = doesMeetAudienceConditions(config: config, experiment: rule, - userId: userId, - attributes: attributes, + user: user, logType: .rolloutRule, loggingKey: loggingKey) reasons.merge(audienceDecisionResponse.reasons) @@ -420,7 +417,7 @@ class DefaultDecisionService: OPTDecisionService { var bucketingId = userId // If the bucketing ID key is defined in attributes, then use that // in place of the userID for the murmur hash key - if let newBucketingId = attributes[Constants.Attributes.OptimizelyBucketIdAttribute] as? String { + if let newBucketingId = attributes[Constants.Attributes.reservedBucketIdAttribute] as? String { bucketingId = newBucketingId } diff --git a/Sources/Implementation/Events/BatchEventBuilder.swift b/Sources/Implementation/Events/BatchEventBuilder.swift index 83b1451e..f2671b26 100644 --- a/Sources/Implementation/Events/BatchEventBuilder.swift +++ b/Sources/Implementation/Events/BatchEventBuilder.swift @@ -16,9 +16,7 @@ import Foundation -class BatchEventBuilder { - static private let swiftSdkClientName = "swift-sdk" - +class BatchEventBuilder { static private var logger = OPTLoggerFactory.getLogger() // MARK: - Impression Event @@ -99,7 +97,7 @@ class BatchEventBuilder { clientVersion: Utils.sdkVersion, visitors: [visitor], projectID: config.project.projectId, - clientName: swiftSdkClientName, + clientName: Utils.swiftSdkClientName, anonymizeIP: config.project.anonymizeIP, enrichDecisions: true) @@ -203,9 +201,9 @@ class BatchEventBuilder { if let botFiltering = config.project.botFiltering, let eventValue = AttributeValue(value: botFiltering) { let botAttr = EventAttribute(value: eventValue, - key: Constants.Attributes.OptimizelyBotFilteringAttribute, + key: Constants.Attributes.reservedBotFilteringAttribute, type: "custom", - entityID: Constants.Attributes.OptimizelyBotFilteringAttribute) + entityID: Constants.Attributes.reservedBotFilteringAttribute) eventAttributes.append(botAttr) } diff --git a/Sources/ODP/LruCache.swift b/Sources/ODP/LruCache.swift new file mode 100644 index 00000000..fc6f9554 --- /dev/null +++ b/Sources/ODP/LruCache.swift @@ -0,0 +1,136 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class LruCache { + + class CacheElement { + var prev: CacheElement? + var next: CacheElement? + let key: K? + let value: V? + var time: TimeInterval + + init(key: K? = nil, value: V? = nil) { + self.key = key + self.value = value + self.time = Date.timeIntervalSinceReferenceDate + } + } + + var map: [K: CacheElement]! + var head: CacheElement! + var tail: CacheElement! + let queue = DispatchQueue(label: "LRU") + let maxSize: Int + let timeoutInSecs: Int + + init(size: Int, timeoutInSecs: Int) { + self.maxSize = size + self.timeoutInSecs = timeoutInSecs + self.reset() + } + + func lookup(key: K) -> V? { + if maxSize <= 0 { return nil } + + var element: CacheElement? + + queue.sync { + element = map[key] + + if let item = element { + removeFromLink(item) + + if isValid(item) { + addToLink(item) + } else { + map[key] = nil + element = nil + } + } + } + + return element?.value + } + + func save(key: K, value: V) { + if maxSize <= 0 { return } + + queue.async(flags: .barrier) { + let oldSegments = self.map[key] + let newSegments = CacheElement(key: key, value: value) + self.map[key] = newSegments + + if let old = oldSegments { + self.removeFromLink(old) + } + self.addToLink(newSegments) + + while self.map.count > self.maxSize { + guard let old = self.head.next, let oldKey = old.key else { break } + self.removeFromLink(old) + self.map[oldKey] = nil + } + } + } + + // read cache contents without order update + func peek(key: K) -> V? { + if maxSize <= 0 { return nil } + + var element: CacheElement? + queue.sync { + element = map[key] + } + return element?.value + } + + func reset() { + if maxSize <= 0 { return } + + queue.sync { + map = [K: CacheElement]() + head = CacheElement() + tail = CacheElement() + head.next = tail + tail.prev = head + } + } + + // MARK: - Utils + + private func removeFromLink(_ item: CacheElement) { + item.prev?.next = item.next + item.next?.prev = item.prev + } + + private func addToLink(_ item: CacheElement) { + let prev = tail.prev! + prev.next = item + tail.prev = item + + item.next = tail + item.prev = prev + } + + private func isValid(_ item: CacheElement) -> Bool { + if timeoutInSecs <= 0 { return true } + return (Date.timeIntervalSinceReferenceDate - item.time) < Double(timeoutInSecs) + } + +} diff --git a/Sources/ODP/OdpConfig.swift b/Sources/ODP/OdpConfig.swift new file mode 100644 index 00000000..e0bb54f7 --- /dev/null +++ b/Sources/ODP/OdpConfig.swift @@ -0,0 +1,138 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class OdpConfig { + /// The host URL for the ODP audience segments API (optional). + private var _apiHost: String? + /// The public API key for the ODP account from which the audience segments will be fetched (optional). + private var _apiKey: String? + /// An array of all ODP segments used in the current datafile (associated with apiHost/apiKey). + private var _segmentsToCheck: [String] + /// An enum value indicating that odp is integrated for the project or not + private var _odpServiceIntegrated: OdpConfigState + + enum OdpConfigState { + case notDetermined + case integrated + case notIntegrated + } + + let queue = DispatchQueue(label: "odpConfig") + + init(apiKey: String? = nil, apiHost: String? = nil, segmentsToCheck: [String] = []) { + self._apiKey = apiKey + self._apiHost = apiHost + self._segmentsToCheck = segmentsToCheck + self._odpServiceIntegrated = .notDetermined // initially queueing allowed until the first datafile is parsed + } + + func update(apiKey: String?, apiHost: String?, segmentsToCheck: [String]) -> Bool { + if (apiKey != nil) && (apiHost != nil) { + self.odpServiceIntegrated = .integrated + } else { + // disable future event queueing if datafile has no ODP integrations. + self.odpServiceIntegrated = .notIntegrated + } + + if self.apiKey == apiKey, self.apiHost == apiHost, self.segmentsToCheck == segmentsToCheck { + return false + } else { + self.apiKey = apiKey + self.apiHost = apiHost + self.segmentsToCheck = segmentsToCheck + return true + } + } +} + +// MARK: - Thread-safe + +extension OdpConfig { + + var apiHost: String? { + get { + var value: String? + queue.sync { + value = _apiHost + } + return value + } + set { + queue.async { + self._apiHost = newValue + } + } + } + + var apiKey: String? { + get { + var value: String? + queue.sync { + value = _apiKey + } + return value + } + set { + queue.async { + self._apiKey = newValue + } + } + } + + var segmentsToCheck: [String] { + get { + var value = [String]() + queue.sync { + value = _segmentsToCheck + } + return value + } + set { + queue.async { + self._segmentsToCheck = newValue + } + } + } + + var odpServiceIntegrated: OdpConfigState { + get { + var value = OdpConfigState.notDetermined + queue.sync { + value = _odpServiceIntegrated + } + return value + } + set { + queue.async { + self._odpServiceIntegrated = newValue + } + } + } + + var eventQueueingAllowed: Bool { + var value = true + queue.sync { + switch _odpServiceIntegrated { + case .notDetermined, .integrated: value = true + case .notIntegrated: value = false + } + } + return value + } + +} diff --git a/Sources/ODP/OdpEvent.swift b/Sources/ODP/OdpEvent.swift new file mode 100644 index 00000000..b97df477 --- /dev/null +++ b/Sources/ODP/OdpEvent.swift @@ -0,0 +1,80 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct OdpEvent: Codable { + let type: String + let action: String + let identifiers: [String: String] + + // [String: Any?] is not Codable. Serialize it before storing and then deserialize when reading back. + let data: [String: Any?] + let dataSerial: Data + + init(type: String, action: String, identifiers: [String: String], data: [String: Any?]) { + self.type = type + self.action = action + self.identifiers = identifiers + self.data = data + + // serialize for DataStoreQueueStackImpl store (Codable required) + self.dataSerial = (try? JSONSerialization.data(withJSONObject: data)) ?? Data() + } + + // for JSON encoding (storing) and decoding (reading back from store) + + enum CodingKeys: String, CodingKey { + case type + case action + case identifiers + case dataSerial + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + self.type = try values.decode(String.self, forKey: .type) + self.action = try values.decode(String.self, forKey: .action) + self.identifiers = try values.decode([String: String].self, forKey: .identifiers) + self.dataSerial = try values.decode(Data.self, forKey: .dataSerial) + + self.data = (try? JSONSerialization.jsonObject(with: dataSerial, options: []) as? [String: Any]) ?? [:] + } + + // For JSON encoding (POST request body) + + var dict: [String: Any] { + return [ + "type": type, + "action": action, + "identifiers": identifiers, + "data": data + ] + } + +} + +extension OdpEvent: Equatable { + + public static func == (lhs: OdpEvent, rhs: OdpEvent) -> Bool { + return lhs.type == rhs.type && + lhs.action == rhs.action && + lhs.identifiers == rhs.identifiers && + lhs.dataSerial == rhs.dataSerial + } + +} diff --git a/Sources/ODP/OdpEventApiManager.swift b/Sources/ODP/OdpEventApiManager.swift new file mode 100644 index 00000000..5034b544 --- /dev/null +++ b/Sources/ODP/OdpEventApiManager.swift @@ -0,0 +1,106 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// ODP REST Events API +// - https://api.zaius.com/v3/events +// - test ODP public API key = "W4WzcEs-ABgXorzY7h1LCQ" + +/* + [Event Request] + + curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"type":"fullstack","action":"identified","identifiers":{"vuid": "123","fs_user_id": "abc"},"data":{"idempotence_id":"xyz","source":"swift-sdk"}}' https://api.zaius.com/v3/events + + [Event Response] + + {"title":"Accepted","status":202,"timestamp":"2022-06-30T20:59:52.046Z"} +*/ + +class OdpEventApiManager { + + func sendOdpEvents(apiKey: String, + apiHost: String, + events: [OdpEvent], + completionHandler: @escaping (OptimizelyError?) -> Void) { + guard let url = URL(string: "\(apiHost)/v3/events") else { + let canRetry = false + completionHandler(.odpEventFailed("Invalid url", canRetry)) + return + } + + guard let body = try? JSONSerialization.data(withJSONObject: events.map { $0.dict }) else { + let canRetry = false + completionHandler(.odpEventFailed("Invalid JSON", canRetry)) + return + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "POST" + urlRequest.httpBody = body + urlRequest.addValue(apiKey, forHTTPHeaderField: "x-api-key") + urlRequest.addValue("application/json", forHTTPHeaderField: "content-type") + + let session = self.getSession() + // without this the URLSession will leak, see docs on URLSession and https://stackoverflow.com/questions/67318867 + defer { session.finishTasksAndInvalidate() } + + let task = session.dataTask(with: urlRequest) { data, response, error in + var errMessage: String? + var canRetry: Bool = true + + defer { + if let errMessage = errMessage { + completionHandler(.odpEventFailed(errMessage, canRetry)) + } else { + completionHandler(nil) + } + } + + if let error = error { + errMessage = error.localizedDescription + return + } + + guard let response = response as? HTTPURLResponse else { + errMessage = "invalid response" + return + } + + var dataStr: String? + if let data = data, let str = String(bytes: data, encoding: .utf8) { + dataStr = str + } + + switch response.statusCode { + case ..<400: + errMessage = nil // success + case 400..<500: + errMessage = dataStr ?? "\(response.statusCode)" + canRetry = false // no retry (client error) + default: + errMessage = "\(response.statusCode)" + } + } + + task.resume() + } + + func getSession() -> URLSession { + return URLSession(configuration: .ephemeral) + } + +} diff --git a/Sources/ODP/OdpEventManager.swift b/Sources/ODP/OdpEventManager.swift new file mode 100644 index 00000000..57d98994 --- /dev/null +++ b/Sources/ODP/OdpEventManager.swift @@ -0,0 +1,194 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +class OdpEventManager { + var odpConfig: OdpConfig + var apiMgr: OdpEventApiManager + + var maxQueueSize = 100 + let maxBatchEvents = 10 + let queueLock: DispatchQueue + let eventQueue: DataStoreQueueStackImpl + + let logger = OPTLoggerFactory.getLogger() + + init(sdkKey: String, odpConfig: OdpConfig? = nil, apiManager: OdpEventApiManager? = nil) { + self.odpConfig = odpConfig ?? OdpConfig() + self.apiMgr = apiManager ?? OdpEventApiManager() + + self.queueLock = DispatchQueue(label: "event") + + // a separate event queue for each sdkKey (which may have own ODP public key) + let storeName = "OPTEvent-ODP-\(sdkKey)" + self.eventQueue = DataStoreQueueStackImpl(queueStackName: "odp", + dataStore: DataStoreFile<[Data]>(storeName: storeName)) + } + + // MARK: - events + + func registerVUID(vuid: String) { + sendEvent(type: Constants.ODP.eventType, + action: "client_initialized", + identifiers: [ + Constants.ODP.keyForVuid: vuid + ], + data: [:]) + } + + func identifyUser(vuid: String, userId: String) { + sendEvent(type: Constants.ODP.eventType, + action: "identified", + identifiers: [ + Constants.ODP.keyForVuid: vuid, + Constants.ODP.keyForUserId: userId + ], + data: [:]) + } + + func sendEvent(type: String, action: String, identifiers: [String: String], data: [String: Any?]) { + let event = OdpEvent(type: type, + action: action, + identifiers: identifiers, + data: addCommonEventData(data)) + dispatch(event) + } + + func addCommonEventData(_ customData: [String: Any?] = [:]) -> [String: Any?] { + var data: [String: Any?] = [ + "idempotence_id": UUID().uuidString, + + "data_source_type": "sdk", + "data_source": Utils.swiftSdkClientName, // "swift-sdk" + "data_source_version": Utils.sdkVersion, // "3.10.2" + + // [optional] client sdks only + "os": Utils.os, // ("iOS", "tvOS", "watchOS", "macOS", "Android", "Windows", "Linux", ...) + "os_version": Utils.osVersion, // "13.2", ... + "device_type": Utils.deviceType, // fixed set = ("Phone", "Tablet", "Smart TV", "Watch", “PC”, "Other") + "model": Utils.deviceModel // ("iPhone 12", "iPad 2", "Pixel 2", "SM-A515F", ...) + + // [optional] + // "data_source_instance": , // if need subtypes of data_source + ] + + data.merge(customData) { (_, custom) in custom } // keep custom data if conflicts + return data + } + + // MARK: - dispatch + + func dispatch(_ event: OdpEvent) { + if eventQueue.count < maxQueueSize { + eventQueue.save(item: event) + } else { + let error = OptimizelyError.eventDispatchFailed("ODP EventQueue is full") + self.logger.e(error) + } + + flush() + } + + func flush() { + guard odpConfig.eventQueueingAllowed else { + // clean up all pending events if datafile becomes ready but has no ODP public key (not integrated) + reset() + return + } + + guard let odpApiKey = odpConfig.apiKey, let odpApiHost = odpConfig.apiHost else { + return + } + + queueLock.async { + func removeStoredEvents(num: Int) { + if let removedItem = self.eventQueue.removeFirstItems(count: num), removedItem.count > 0 { + // avoid event-log-message preparation overheads with closure-logging + self.logger.d({ "ODP: Removed stored \(num) events starting with \(removedItem.first!)" }) + } else { + self.logger.e("ODP: Failed to removed \(num) events") + } + } + + // sync group used to ensure that the sendEvent is synchronous. + // used in flushEvents + let sync = DispatchGroup() + + while let events: [OdpEvent] = self.eventQueue.getFirstItems(count: self.maxBatchEvents) { + let numEvents = events.count + + // multiple auto-retries are disabled for now + // - this may be too much since they'll be retried any way when next events arrive. + // - also, no guarantee on success after multiple retries, so it helps minimal with extra complexity. + + var odpError: OptimizelyError? + + sync.enter() // make the send event synchronous. enter our notify + self.apiMgr.sendOdpEvents(apiKey: odpApiKey, + apiHost: odpApiHost, + events: events) { error in + odpError = error + sync.leave() // our send is done. + } + sync.wait() // wait for send completed + + if let error = odpError { + self.logger.e(error.reason) + + // retry only if needed (non-permanent) + if case .odpEventFailed(_, let canRetry) = error { + if canRetry { + // keep the failed event queue so it can be re-sent later + break + } else { + // permanent errors (400 response or invalid events, etc) + // discard these events so that they do not block following valid events + } + } + } + + removeStoredEvents(num: numEvents) + } + } + } + + func reset() { + _ = eventQueue.removeFirstItems(count: self.maxQueueSize) + } + + // MARK: - Utils + + /// Validate if data has all valid types only (string, integer, float, boolean, and nil), + /// - Parameter data: a dictionary. + /// - Returns: true if all values are valid types. + func isDataValidType(_ data: [String: Any?]) -> Bool { + for value in data.values { + if let v = value { + if Utils.isStringType(v) || Utils.isIntType(v) || Utils.isDoubleType(v) || Utils.isBoolType(v) { + continue + } else { + return false // not a nil or a valid type + } + } else { + continue // nil should be accepted + } + } + + return true + } +} diff --git a/Sources/ODP/OdpManager.swift b/Sources/ODP/OdpManager.swift new file mode 100644 index 00000000..28db371a --- /dev/null +++ b/Sources/ODP/OdpManager.swift @@ -0,0 +1,153 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class OdpManager { + var enabled: Bool + var vuidManager: OdpVuidManager + + var odpConfig: OdpConfig! + var segmentManager: OdpSegmentManager! + var eventManager: OdpEventManager! + + let logger = OPTLoggerFactory.getLogger() + + var vuid: String { + return vuidManager.vuid + } + + init(sdkKey: String, + disable: Bool, + cacheSize: Int, + cacheTimeoutInSecs: Int, + segmentManager: OdpSegmentManager? = nil, + eventManager: OdpEventManager? = nil) { + + self.enabled = !disable + self.vuidManager = OdpVuidManager.shared + + guard enabled else { + logger.i(.odpNotEnabled) + return + } + + self.odpConfig = OdpConfig() + + if let segmentManager = segmentManager { + segmentManager.odpConfig = odpConfig + self.segmentManager = segmentManager + } else { + self.segmentManager = OdpSegmentManager(cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeoutInSecs, + odpConfig: odpConfig) + } + + if let eventManager = eventManager { + eventManager.odpConfig = odpConfig + self.eventManager = eventManager + } else { + self.eventManager = OdpEventManager(sdkKey: sdkKey, odpConfig: odpConfig) + } + + self.eventManager.registerVUID(vuid: self.vuidManager.vuid) + } + + func fetchQualifiedSegments(userId: String, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + guard enabled else { + completionHandler(nil, .odpNotEnabled) + return + } + + let userKey = vuidManager.isVuid(visitorId: userId) ? Constants.ODP.keyForVuid : Constants.ODP.keyForUserId + let userValue = userId + + segmentManager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + options: options, + completionHandler: completionHandler) + } + + func identifyUser(userId: String) { + guard enabled else { + logger.d("ODP identify event is not dispatched (ODP disabled).") + return + } + + guard odpConfig.eventQueueingAllowed else { + logger.d("ODP identify event is not dispatched (ODP not integrated).") + return + } + + eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) + } + + /// Send an event to the ODP server. + /// + /// - Parameters: + /// - type: the event type. + /// - action: the event action name. + /// - identifiers: a dictionary for identifiers. + /// - data: a dictionary for associated data. The default event data will be added to this data before sending to the ODP server. + /// - Throws: `OptimizelyError` if error is detected + func sendEvent(type: String, action: String, identifiers: [String: String], data: [String: Any?]) throws { + guard enabled else { throw OptimizelyError.odpNotEnabled } + guard odpConfig.eventQueueingAllowed else { throw OptimizelyError.odpNotIntegrated } + guard eventManager.isDataValidType(data) else { throw OptimizelyError.odpInvalidData } + + var identifiersWithVuid = identifiers + if identifiers[Constants.ODP.keyForVuid] == nil { + identifiersWithVuid[Constants.ODP.keyForVuid] = vuidManager.vuid + } + + eventManager.sendEvent(type: type, action: action, identifiers: identifiersWithVuid, data: data) + } + + func updateOdpConfig(apiKey: String?, apiHost: String?, segmentsToCheck: [String]) { + guard enabled else { return } + + // flush old events using old odp publicKey (if exists) before updating odp key. + // NOTE: It should be rare but possible that odp public key is changed for the same datafile (sdkKey). + // Try to send all old events with the previous public key. + // If it fails to flush all the old events here (network error), remaning events will be discarded. + eventManager.flush() + + let configChanged = odpConfig.update(apiKey: apiKey, + apiHost: apiHost, + segmentsToCheck: segmentsToCheck) + if configChanged { + // reset segments cache when odp integration or segmentsToCheck are changed + segmentManager.reset() + } + } + +} + +extension OdpManager: BackgroundingCallbacks { + func applicationDidEnterBackground() { + guard enabled else { return } + + eventManager.flush() + } + + func applicationDidBecomeActive() { + guard enabled else { return } + + // no actions here for now + } +} diff --git a/Sources/ODP/OdpSegmentApiManager.swift b/Sources/ODP/OdpSegmentApiManager.swift new file mode 100644 index 00000000..ef77993a --- /dev/null +++ b/Sources/ODP/OdpSegmentApiManager.swift @@ -0,0 +1,235 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// ODP GraphQL API +// - https://api.zaius.com/v3/graphql +// - test ODP public API key = "W4WzcEs-ABgXorzY7h1LCQ" + +/* + + [GraphQL Request] + + // fetch info with fs_user_id for ["has_email", "has_email_opted_in", "push_on_sale"] segments + curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(fs_user_id: \"tester-101\") {audiences(subset:[\"has_email\",\"has_email_opted_in\",\"push_on_sale\"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql + + // fetch info with vuid for ["has_email", "has_email_opted_in", "push_on_sale"] segments + curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences(subset:[\"has_email\",\"has_email_opted_in\",\"push_on_sale\"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql + + query MyQuery { + customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { + audiences(subset:["has_email","has_email_opted_in","push_on_sale"]) { + edges { + node { + name + state + } + } + } + } + } + + [GraphQL Response] + + { + "data": { + "customer": { + "audiences": { + "edges": [ + { + "node": { + "name": "has_email", + "state": "qualified", + } + }, + { + "node": { + "name": "has_email_opted_in", + "state": "qualified", + } + }, + ... + ] + } + } + } + } + + [GraphQL Error Response] + + { + "errors": [ + { + "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "customer" + ], + "extensions": { + "classification": "InvalidIdentifierException" + } + } + ], + "data": { + "customer": null + } + } +*/ + +class OdpSegmentApiManager { + let logger = OPTLoggerFactory.getLogger() + + func fetchSegments(apiKey: String, + apiHost: String, + userKey: String, + userValue: String, + segmentsToCheck: [String], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + + let query = makeQuery(userKey: userKey, userValue: userValue, segmentsToCheck: segmentsToCheck) + guard let httpBody = try? JSONSerialization.data(withJSONObject: query) else { + completionHandler(nil, .fetchSegmentsFailed("invalid query.")) + return + } + + let apiEndpoint = apiHost + "/v3/graphql" + let url = URL(string: apiEndpoint)! + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "POST" + urlRequest.httpBody = httpBody + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.setValue(apiKey, forHTTPHeaderField: "x-api-key") + + let session = self.getSession() + // without this the URLSession will leak, see docs on URLSession and https://stackoverflow.com/questions/67318867 + defer { session.finishTasksAndInvalidate() } + + let task = session.dataTask(with: urlRequest) { data, response, error in + var returnError: OptimizelyError? + var returnSegments: [String]? + + defer { + completionHandler(returnSegments, returnError) + } + + guard error == nil, let data = data, let response = response as? HTTPURLResponse else { + let msg = error?.localizedDescription ?? "invalid response" + self.logger.d { + "GraphQL download failed: \(msg)" + } + returnError = .fetchSegmentsFailed("network error") + return + } + + let status = response.statusCode + guard status < 400 else { + returnError = .fetchSegmentsFailed("\(status)") + return + } + + guard let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + returnError = .fetchSegmentsFailed("decode error") + return + } + + // most meaningful ODP errors are returned in 200 success JSON under {"errors": ...} + if let odpErrors: [[String: Any]] = dict.extractComponent(keyPath: "errors") { + if let odpError = odpErrors.first, let errorClass: String = odpError.extractComponent(keyPath: "extensions.classification") { + if errorClass == "InvalidIdentifierException" { + returnError = .invalidSegmentIdentifier + } else { + returnError = .fetchSegmentsFailed(errorClass) + } + return + } + } + + guard let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") else { + returnError = .fetchSegmentsFailed("decode error") + return + } + + let audiences = audDict.compactMap { OdpAudience($0["node"] as? [String: Any]) } + returnSegments = audiences.filter { $0.isQualified }.map { $0.name } + } + + task.resume() + } + + func getSession() -> URLSession { + return URLSession(configuration: .ephemeral) + } + + func makeQuery(userKey: String, userValue: String, segmentsToCheck: [String]) -> [String: Any] { + return [ + "query": "query($userId: String, $audiences: [String]) {customer(\(userKey): $userId) {audiences(subset: $audiences) {edges {node {name state}}}}}", + "variables": [ + "userId": userValue, + "audiences": segmentsToCheck + ] + ] + } + +} + +// MARK: - Utils + +struct OdpAudience: Decodable { + let name: String + let state: String + let description: String? // optional so we can add for debugging + + var isQualified: Bool { + return (state == "qualified") + } + + init?(_ dict: [String: Any]?) { + guard let dict = dict, + let name = dict["name"] as? String, + let state = dict["state"] as? String else { return nil } + + self.name = name + self.state = state + self.description = dict["description"] as? String + } +} + +// Extract deep-json contents with keypath "a.b.c" +// { "a": { "b": { "c": "contents" } } } + +extension Dictionary { + + func extractComponent(keyPath: String) -> T? { + var current: Any? = self + + for component in keyPath.split(separator: ".") { + if let dictionary = current as? [String: Any] { + current = dictionary[String(component)] + } else { + return nil + } + } + + return current == nil ? nil : (current as? T) + } + +} diff --git a/Sources/ODP/OdpSegmentManager.swift b/Sources/ODP/OdpSegmentManager.swift new file mode 100644 index 00000000..37fe5c1d --- /dev/null +++ b/Sources/ODP/OdpSegmentManager.swift @@ -0,0 +1,97 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class OdpSegmentManager { + var odpConfig: OdpConfig + var segmentsCache: LruCache + var apiMgr: OdpSegmentApiManager + + let logger = OPTLoggerFactory.getLogger() + + init(cacheSize: Int, + cacheTimeoutInSecs: Int, + odpConfig: OdpConfig? = nil, + apiManager: OdpSegmentApiManager? = nil) { + self.odpConfig = odpConfig ?? OdpConfig() + self.apiMgr = apiManager ?? OdpSegmentApiManager() + + self.segmentsCache = LruCache(size: cacheSize, + timeoutInSecs: cacheTimeoutInSecs) + } + + func fetchQualifiedSegments(userKey: String, + userValue: String, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + guard let odpApiKey = odpConfig.apiKey, let odpApiHost = odpConfig.apiHost else { + completionHandler(nil, .fetchSegmentsFailed("apiKey/apiHost not defined")) + return + } + + // empty segmentsToCheck (no ODP audiences found in datafile) is not an error. return immediately without checking with the ODP server. + let segmentsToCheck = odpConfig.segmentsToCheck + guard segmentsToCheck.count > 0 else { + completionHandler([], nil) + return + } + + let cacheKey = makeCacheKey(userKey, userValue) + + let ignoreCache = options.contains(.ignoreCache) + let resetCache = options.contains(.resetCache) + + if resetCache { + reset() + } + + if !ignoreCache { + if let segments = segmentsCache.lookup(key: cacheKey) { + completionHandler(segments, nil) + return + } + } + + apiMgr.fetchSegments(apiKey: odpApiKey, + apiHost: odpApiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: segmentsToCheck) { segments, err in + if err == nil, let segments = segments { + if !ignoreCache { + self.segmentsCache.save(key: cacheKey, value: segments) + } + } + + completionHandler(segments, err) + } + } + + func reset() { + segmentsCache.reset() + } +} + +// MARK: - Utils + +extension OdpSegmentManager { + + func makeCacheKey(_ userKey: String, _ userValue: String) -> String { + return userKey + "-$-" + userValue + } + +} diff --git a/Sources/ODP/OdpVuidManager.swift b/Sources/ODP/OdpVuidManager.swift new file mode 100644 index 00000000..6c882041 --- /dev/null +++ b/Sources/ODP/OdpVuidManager.swift @@ -0,0 +1,67 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class OdpVuidManager { + var vuid: String = "" + let logger = OPTLoggerFactory.getLogger() + + // a single vuid should be shared for all SDK instances + static let shared = OdpVuidManager() + + init() { + self.vuid = load() + } + + func makeVuid() -> String { + let maxLength = 32 // required by ODP server + + // make sure UUIDv4 is used (not UUIDv1 or UUIDv6) since the trailing 5 chars will be truncated. See TDD for details. + let vuidFull = "vuid_" + UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased() + let vuid = (vuidFull.count <= maxLength) ? vuidFull : String(vuidFull.prefix(maxLength)) + return vuid + } + + func isVuid(visitorId: String) -> Bool { + return visitorId.starts(with: "vuid_") + } +} + +// MARK: - VUID Store + +extension OdpVuidManager { + + private var keyForVuid: String { + return "optimizely-vuid" + } + + private func load() -> String { + if let oldVuid = UserDefaults.standard.string(forKey: keyForVuid) { + return oldVuid + } + + let vuid = makeVuid() + save(vuid: vuid) + return vuid + } + + private func save(vuid: String) { + UserDefaults.standard.set(vuid, forKey: keyForVuid) + UserDefaults.standard.synchronize() + } + +} diff --git a/Sources/ODP/OptimizelySdkSettings.swift b/Sources/ODP/OptimizelySdkSettings.swift new file mode 100644 index 00000000..38afe4c3 --- /dev/null +++ b/Sources/ODP/OptimizelySdkSettings.swift @@ -0,0 +1,40 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public struct OptimizelySdkSettings { + /// The maximum size of audience segments cache - cache is disabled if this is set to zero. + let segmentsCacheSize: Int + /// The timeout in seconds of audience segments cache - timeout is disabled if this is set to zero. + let segmentsCacheTimeoutInSecs: Int + /// ODP features are disabled if this is set to true. + let disableOdp: Bool + + /// Optimizely SDK Settings + /// + /// - Parameters: + /// - segmentsCacheSize: The maximum size of audience segments cache (optional. default = 100). Set to zero to disable caching. + /// - segmentsCacheTimeoutInSecs: The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout. + /// - disableOdp: Set this flag to true (default = false) to disable ODP features + public init(segmentsCacheSize: Int = 100, + segmentsCacheTimeoutInSecs: Int = 600, + disableOdp: Bool = false) { + self.segmentsCacheSize = segmentsCacheSize + self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs + self.disableOdp = disableOdp + } +} diff --git a/Sources/ODP/OptimizelySegmentOption.swift b/Sources/ODP/OptimizelySegmentOption.swift new file mode 100644 index 00000000..dd186ca1 --- /dev/null +++ b/Sources/ODP/OptimizelySegmentOption.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Options controlling audience segments. +@objc public enum OptimizelySegmentOption: Int { + // ignore cache (save/lookup) + case ignoreCache + // reset cache + case resetCache +} diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index 629afeb5..acae720c 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -31,6 +31,14 @@ extension OptimizelyClient { return OptimizelyUserContext(optimizely: self, userId: userId, attributes: attributes) } + /// Create a context with the device vuid for which decision APIs will be called. + /// + /// - Parameter attributes: A map of attribute names to current user attribute values. + /// - Returns: An OptimizelyUserContext associated with this OptimizelyClient + public func createUserContext(attributes: [String: Any]? = nil) -> OptimizelyUserContext { + return OptimizelyUserContext(optimizely: self, userId: vuid, attributes: attributes) + } + func createUserContext(userId: String, attributes: OptimizelyAttributes? = nil) -> OptimizelyUserContext { return createUserContext(userId: userId, diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 80a78599..1a2175dd 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -20,25 +20,45 @@ import Foundation public class OptimizelyUserContext { weak var optimizely: OptimizelyClient? public var userId: String - - var atomicAttributes: AtomicProperty<[String: Any?]> + + private var atomicAttributes: AtomicProperty<[String: Any?]> public var attributes: [String: Any?] { return atomicAttributes.property ?? [:] } - var forcedDecisions: AtomicDictionary? + private var atomicForcedDecisions: AtomicProperty<[OptimizelyDecisionContext: OptimizelyForcedDecision]> + var forcedDecisions: [OptimizelyDecisionContext: OptimizelyForcedDecision]? { + return atomicForcedDecisions.property + } + private var atomicQualifiedSegments: AtomicProperty<[String]> + /// an array of segment names that the user is qualified for. The result of **fetchQualifiedSegments()** will be saved here. + public var qualifiedSegments: [String]? { + get { + return atomicQualifiedSegments.property + } + // keep this public set api for clients to set directly (testing/debugging) + set { + atomicQualifiedSegments.property = newValue + } + } + var clone: OptimizelyUserContext? { guard let optimizely = self.optimizely else { return nil } - let userContext = OptimizelyUserContext(optimizely: optimizely, userId: userId, attributes: attributes) + let userContext = OptimizelyUserContext(optimizely: optimizely, userId: userId, attributes: attributes, identify: false) + if let fds = forcedDecisions { - userContext.forcedDecisions = AtomicDictionary(fds.property) + userContext.atomicForcedDecisions.property = fds + } + + if let qs = qualifiedSegments { + userContext.atomicQualifiedSegments.property = qs } return userContext } - + let logger = OPTLoggerFactory.getLogger() /// OptimizelyUserContext init @@ -47,14 +67,32 @@ public class OptimizelyUserContext { /// - optimizely: An instance of OptimizelyClient to be used for decisions. /// - userId: The user ID to be used for bucketing. /// - attributes: A map of attribute names to current user attribute values. - public init(optimizely: OptimizelyClient, - userId: String, - attributes: [String: Any?]? = nil) { + public convenience init(optimizely: OptimizelyClient, + userId: String, + attributes: [String: Any?]? = nil) { + self.init(optimizely: optimizely, userId: userId, attributes: attributes ?? [:], identify: true) + } + + init(optimizely: OptimizelyClient, + userId: String, + attributes: [String: Any?], + identify: Bool) { self.optimizely = optimizely self.userId = userId - self.atomicAttributes = AtomicProperty(property: attributes ?? [:]) + + let lock = DispatchQueue(label: "user-context") + self.atomicAttributes = AtomicProperty(property: attributes, lock: lock) + self.atomicForcedDecisions = AtomicProperty(property: nil, lock: lock) + self.atomicQualifiedSegments = AtomicProperty(property: nil, lock: lock) + + if identify { + // async call so event building overhead is not blocking context creation + lock.async { + self.optimizely?.identifyUserToOdp(userId: userId) + } + } } - + /// Sets an attribute for a given key. /// - Parameters: /// - key: An attribute key @@ -139,6 +177,53 @@ public class OptimizelyUserContext { } +// MARK: - ODP + +extension OptimizelyUserContext { + + /// Fetch all qualified segments for the user context. + /// + /// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time. + /// On failure, **qualifiedSegments** will be nil and one of these errors will be returned: + /// - OptimizelyError.invalidSegmentIdentifier + /// - OptimizelyError.fetchSegmentsFailed(String) + /// + /// - Parameters: + /// - options: A set of options for fetching qualified segments (optional). + /// - completionHandler: A completion handler to be called with the fetch result. On success, it'll pass a non-nil segments array (can be empty) with a nil error. On failure, it'll pass a non-nil error with a nil segments array. + public func fetchQualifiedSegments(options: [OptimizelySegmentOption] = [], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + // on failure, qualifiedSegments should be reset if a previous value exists. + self.atomicQualifiedSegments.property = nil + + guard let optimizely = self.optimizely else { + completionHandler(nil, .sdkNotReady) + return + } + + optimizely.fetchQualifiedSegments(userId: userId, options: options) { segments, err in + guard err == nil, let segments = segments else { + let error = err ?? OptimizelyError.fetchSegmentsFailed("invalid segments") + self.logger.e(error) + completionHandler(nil, error) + return + } + + self.atomicQualifiedSegments.property = segments + completionHandler(segments, nil) + } + } + + /// Check if the user is qualified for the given segment. + /// + /// - Parameter segment: the segment name to check qualification for. + /// - Returns: true if qualified. + public func isQualifiedFor(segment: String) -> Bool { + return atomicQualifiedSegments.property?.contains(segment) ?? false + } + +} + // MARK: - ForcedDecisions /// Decision Context @@ -153,7 +238,7 @@ public struct OptimizelyDecisionContext: Hashable { } /// Forced Decision -public struct OptimizelyForcedDecision { +public struct OptimizelyForcedDecision: Equatable { public let variationKey: String public init(variationKey: String) { @@ -172,10 +257,13 @@ extension OptimizelyUserContext { // create on the first setForcedDecision call if forcedDecisions == nil { - forcedDecisions = AtomicDictionary() + atomicForcedDecisions.property = [:] + } + + atomicForcedDecisions.performAtomic { property in + property[context] = decision } - forcedDecisions![context] = decision return true } @@ -184,9 +272,7 @@ extension OptimizelyUserContext { /// - context: A decision context /// - Returns: A forced decision or nil if forced decisions are not set for the decision context. public func getForcedDecision(context: OptimizelyDecisionContext) -> OptimizelyForcedDecision? { - guard let fds = forcedDecisions else { return nil } - - return fds[context] + return atomicForcedDecisions.property?[context] } /// Removes the forced decision for a given decision context. @@ -194,23 +280,19 @@ extension OptimizelyUserContext { /// - context: A decision context. /// - Returns: true if the forced decision has been removed successfully. public func removeForcedDecision(context: OptimizelyDecisionContext) -> Bool { - guard let fds = forcedDecisions else { return false } - - if getForcedDecision(context: context) != nil { - fds[context] = nil - return true + var exist = false + atomicForcedDecisions.performAtomic { property in + exist = property[context] != nil + property[context] = nil } - return false + return exist } /// Removes all forced decisions bound to this user context. /// - Returns: true if forced decisions have been removed successfully. public func removeAllForcedDecisions() -> Bool { - if let fds = forcedDecisions { - fds.removeAll() - } - + atomicForcedDecisions.property = nil return true } @@ -222,7 +304,9 @@ extension OptimizelyUserContext: Equatable { public static func == (lhs: OptimizelyUserContext, rhs: OptimizelyUserContext) -> Bool { return lhs.userId == rhs.userId && - (lhs.attributes as NSDictionary).isEqual(to: rhs.attributes as [AnyHashable: Any]) + (lhs.attributes as NSDictionary).isEqual(to: rhs.attributes as [AnyHashable: Any]) && + lhs.forcedDecisions == rhs.forcedDecisions && + lhs.qualifiedSegments == rhs.qualifiedSegments } } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index a88fd9de..04617f95 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -1,5 +1,5 @@ // -// Copyright 2019-2021, Optimizely, Inc. and contributors +// Copyright 2019-2022, Optimizely, Inc. and contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,6 +32,12 @@ open class OptimizelyClient: NSObject { } set { atomicConfig.property = newValue + + if let newValue = newValue { + odpManager.updateOdpConfig(apiKey: newValue.publicKeyForODP, + apiHost: newValue.hostForODP, + segmentsToCheck: newValue.allSegments) + } } } @@ -48,11 +54,13 @@ open class OptimizelyClient: NSObject { var logger: OPTLogger! var eventDispatcher: OPTEventDispatcher? public var datafileHandler: OPTDatafileHandler? - + // MARK: - Default Services var decisionService: OPTDecisionService! public var notificationCenter: OPTNotificationCenter? + var odpManager: OdpManager + let sdkSettings: OptimizelySdkSettings // MARK: - Public interfaces @@ -65,18 +73,26 @@ open class OptimizelyClient: NSObject { /// - datafileHandler: custom datafile handler (optional) /// - userProfileService: custom UserProfileService (optional) /// - defaultLogLevel: default log level (optional. default = .info) - /// - defaultDecisionOptions: default decision optiopns (optional) + /// - defaultDecisionOptions: default decision options (optional) + /// - settings: SDK configuration (optional) public init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, datafileHandler: OPTDatafileHandler? = nil, userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil) { + defaultDecideOptions: [OptimizelyDecideOption]? = nil, + settings: OptimizelySdkSettings? = nil) { self.sdkKey = sdkKey + self.sdkSettings = settings ?? OptimizelySdkSettings() self.defaultDecideOptions = defaultDecideOptions ?? [] + self.odpManager = OdpManager(sdkKey: sdkKey, + disable: sdkSettings.disableOdp, + cacheSize: sdkSettings.segmentsCacheSize, + cacheTimeoutInSecs: sdkSettings.segmentsCacheTimeoutInSecs) + super.init() let userProfileService = userProfileService ?? DefaultUserProfileService() @@ -95,7 +111,7 @@ open class OptimizelyClient: NSObject { self.datafileHandler = HandlerRegistryService.shared.injectDatafileHandler(sdkKey: self.sdkKey) self.decisionService = HandlerRegistryService.shared.injectDecisionService(sdkKey: self.sdkKey) self.notificationCenter = HandlerRegistryService.shared.injectNotificationCenter(sdkKey: self.sdkKey) - + logger.d("SDK Version: \(version)") } @@ -189,7 +205,7 @@ open class OptimizelyClient: NSObject { func configSDK(datafile: Data) throws { do { self.config = try ProjectConfig(datafile: datafile) - + datafileHandler?.startUpdates(sdkKey: self.sdkKey) { data in // new datafile came in self.updateConfigFromBackgroundFetch(data: data) @@ -747,6 +763,7 @@ open class OptimizelyClient: NSObject { return OptimizelyConfigImp(projectConfig: config) } + } // MARK: - Send Events @@ -910,6 +927,47 @@ extension OptimizelyClient { } +// MARK: - ODP + +extension OptimizelyClient { + + /// Send an event to the ODP server. + /// + /// - Parameters: + /// - type: the event type (default = "fullstack"). + /// - action: the event action name. + /// - identifiers: a dictionary for identifiers. + /// - data: a dictionary for associated data. The default event data will be added to this data before sending to the ODP server. + /// - Throws: `OptimizelyError` if error is detected + public func sendOdpEvent(type: String? = nil, + action: String, + identifiers: [String: String] = [:], + data: [String: Any?] = [:]) throws { + try odpManager.sendEvent(type: type ?? Constants.ODP.eventType, + action: action, + identifiers: identifiers, + data: data) + } + + /// the device vuid (read only) + public var vuid: String { + return odpManager.vuid + } + + func identifyUserToOdp(userId: String) { + odpManager.identifyUser(userId: userId) + } + + func fetchQualifiedSegments(userId: String, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + odpManager.fetchQualifiedSegments(userId: userId, + options: options, + completionHandler: completionHandler) + } + +} + // MARK: - For test support extension OptimizelyClient { diff --git a/Sources/Optimizely/OptimizelyError.swift b/Sources/Optimizely/OptimizelyError.swift index 2d0766ab..cd5fd03d 100644 --- a/Sources/Optimizely/OptimizelyError.swift +++ b/Sources/Optimizely/OptimizelyError.swift @@ -79,6 +79,15 @@ public enum OptimizelyError: Error { case eventDispatchFailed(_ reason: String) case eventDispatcherConfigError(_ reason: String) + + // MARK: - AudienceSegements Errors + + case invalidSegmentIdentifier + case fetchSegmentsFailed(_ hint: String) + case odpEventFailed(_ hint: String, _ canRetry: Bool) + case odpNotIntegrated + case odpNotEnabled + case odpInvalidData } // MARK: - CustomStringConvertible @@ -147,6 +156,13 @@ extension OptimizelyError: CustomStringConvertible, ReasonProtocol { case .eventDispatchFailed(let hint): message = "Event dispatch failed (\(hint))." case .eventDispatcherConfigError(let hint): message = "EventDispatcher config error (\(hint))." + + case .invalidSegmentIdentifier: message = "Audience segments fetch failed (invalid identifier)" + case .fetchSegmentsFailed(let hint): message = "Audience segments fetch failed (\(hint))." + case .odpEventFailed(let hint, _): message = "ODP event send failed (\(hint))." + case .odpNotIntegrated: message = "ODP is not integrated." + case .odpNotEnabled: message = "ODP is not enabled." + case .odpInvalidData: message = "ODP data is not valid." } return message diff --git a/Sources/Utils/AtomicArray.swift b/Sources/Utils/AtomicArray.swift index f96843cd..f16e7f26 100644 --- a/Sources/Utils/AtomicArray.swift +++ b/Sources/Utils/AtomicArray.swift @@ -16,7 +16,7 @@ import Foundation -class AtomicArray: AtomicWrapper { +class AtomicArray: AtomicWrapper { private var _property: [T] var property: [T] { @@ -67,6 +67,12 @@ class AtomicArray: AtomicWrapper { } } + func contains(_ item: T) -> Bool { + return getAtomic { + _property.contains(item) + } ?? false + } + func firstIndex(where predicate: (T) throws -> Bool) rethrows -> Int? { return getAtomic { try _property.firstIndex(where: predicate) diff --git a/Sources/Utils/AtomicDictionary.swift b/Sources/Utils/AtomicDictionary.swift index b735a63e..91243132 100644 --- a/Sources/Utils/AtomicDictionary.swift +++ b/Sources/Utils/AtomicDictionary.swift @@ -16,7 +16,7 @@ import Foundation -class AtomicDictionary: AtomicWrapper where K: Hashable { +class AtomicDictionary: AtomicWrapper { private var _property: [K: V] var property: [K: V] { diff --git a/Sources/Utils/AtomicProperty.swift b/Sources/Utils/AtomicProperty.swift index acd29469..46ad2c32 100644 --- a/Sources/Utils/AtomicProperty.swift +++ b/Sources/Utils/AtomicProperty.swift @@ -32,24 +32,25 @@ class AtomicProperty { } } } - private let lock: DispatchQueue = { - var name = "AtomicProperty" + String(Int.random(in: 0...100000)) - let clzzName = String(describing: T.self) - name += clzzName - return DispatchQueue(label: name, attributes: .concurrent) - }() + private let lock: DispatchQueue - init(property: T) { - self.property = property + init(property: T?, lock: DispatchQueue? = nil) { + self._property = property + self.lock = lock ?? { + var name = "AtomicProperty" + String(Int.random(in: 0...100000)) + let className = String(describing: T.self) + name += className + return DispatchQueue(label: name, attributes: .concurrent) + }() } - init() { - + convenience init() { + self.init(property: nil, lock: nil) } // perform an atomic operation on the atomic property // the operation will not run if the property is nil. - public func performAtomic(atomicOperation: ((_ prop:inout T) -> Void)) { + func performAtomic(atomicOperation: (_ prop:inout T) -> Void) { lock.sync(flags: DispatchWorkItemFlags.barrier) { if var prop = _property { atomicOperation(&prop) diff --git a/Sources/Utils/Constants.swift b/Sources/Utils/Constants.swift index 04ccb1fa..b9c65f05 100644 --- a/Sources/Utils/Constants.swift +++ b/Sources/Utils/Constants.swift @@ -18,9 +18,15 @@ import Foundation struct Constants { struct Attributes { - static let OptimizelyBucketIdAttribute = "$opt_bucketing_id" - static let OptimizelyBotFilteringAttribute = "$opt_bot_filtering" - static let OptimizelyUserAgent = "$opt_user_agent" + static let reservedBucketIdAttribute = "$opt_bucketing_id" + static let reservedBotFilteringAttribute = "$opt_bot_filtering" + static let reservedUserAgent = "$opt_user_agent" + } + + struct ODP { + static let keyForVuid = "vuid" + static let keyForUserId = "fs_user_id" + static let eventType = "fullstack" } enum EvaluationLogType: String { diff --git a/Sources/Utils/Utils.swift b/Sources/Utils/Utils.swift index fc285429..60068f24 100644 --- a/Sources/Utils/Utils.swift +++ b/Sources/Utils/Utils.swift @@ -15,12 +15,63 @@ // import Foundation +#if os(watchOS) +import WatchKit +#else +import UIKit +#endif class Utils { // from auto-generated variable OPTIMIZELYSDKVERSION static var sdkVersion: String = OPTIMIZELYSDKVERSION + static let swiftSdkClientName = "swift-sdk" + + static var os: String { + #if os(iOS) + return "iOS" + #elseif os(tvOS) + return "tvOS" + #elseif os(macOS) + return "macOS" + #elseif os(watchOS) + return "watchOS" + #else + return "Other" + #endif + } + + static var osVersion: String { + #if os(watchOS) + return WKInterfaceDevice.current().systemVersion + #else + return UIDevice.current.systemVersion + #endif + } + + static var deviceModel: String { + #if os(watchOS) + return WKInterfaceDevice.current().model + #else + return UIDevice.current.model + #endif + } + static var deviceType: String { + // UIUserInterfaceIdiom is an alternative solution, but some (.mac, etc) behaves in an unexpected way. + #if os(iOS) + return (UIDevice.current.userInterfaceIdiom == .phone) ? "Phone" : "Tablet" + #elseif os(tvOS) + return "Smart TV" + #elseif os(macOS) + return "PC" + #elseif os(watchOS) + return "Watch" + #else + return "Other" + #endif + } + private static let jsonEncoder = JSONEncoder() // @objc NSNumber can be casted either Bool, Int, or Double @@ -51,6 +102,10 @@ class Utils { return isSwiftNumType || isNSNumberDoubleType(value) } + static func isStringType(_ value: Any) -> Bool { + return (value is String) + } + // MARK: - NSNumber static func isNSNumberBoolType(_ value: Any) -> Bool { diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests.swift index de58250b..f103c765 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests.swift @@ -44,7 +44,8 @@ class OptimizelyClientTests: XCTestCase { func testTypedAudienceThroughProject() { // let variation = try? optimizely?.activate(experimentKey: "typed_audience_experiment", userId: "userId", attributes: ["doubleKey":5]) - let answer = try? optimizely?.config?.project.evaluateAudience(audienceId: "3468206643", attributes: ["booleanKey": true]) + let answer = try? optimizely?.config?.project.evaluateAudience(audienceId: "3468206643", + user: OTUtils.user(attributes: ["booleanKey": true])) XCTAssertTrue(answer!) } diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Decide.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Decide.swift index c29a6c0d..c67ba7b7 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Decide.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Decide.swift @@ -45,6 +45,22 @@ class OptimizelyClientTests_Decide: XCTestCase { XCTAssert(user.attributes["old"] as! Bool == true) } + func testCreateUserContext_vuid() { + let attributes: [String: Any] = [ + "country": "us", + "age": 100, + "old": true + ] + + let user = optimizely.createUserContext(attributes: attributes) + + XCTAssert(user.optimizely == optimizely) + XCTAssert(user.userId == optimizely.vuid, "vuid should be used as the default userId when not given") + XCTAssert(user.attributes["country"] as! String == "us") + XCTAssert(user.attributes["age"] as! Int == 100) + XCTAssert(user.attributes["old"] as! Bool == true) + } + func testCreateUserContext_multiple() { let attributes: [String: Any] = [ "country": "us", diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift index bd1e3701..4c8bc9c0 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift @@ -35,7 +35,7 @@ class OptimizelyClientTests_Evaluation: XCTestCase { let attributes: [String: Any?] = [ "i_42": -9007199254740994 - ] + ] let variationKey = try? optimizely.activate(experimentKey: experimentKey, userId: userId, attributes: attributes) XCTAssertNil(variationKey) @@ -49,7 +49,7 @@ class OptimizelyClientTests_Evaluation: XCTestCase { let userId = "test_user_1" let attributes: [String: Any?] = [ - "i_42": 9007199254740994 + "i_42": Int64(9007199254740994) ] let variationKey = try? optimizely.activate(experimentKey: experimentKey, userId: userId, attributes: attributes) @@ -64,7 +64,7 @@ class OptimizelyClientTests_Evaluation: XCTestCase { let userId = "test_user_1" let attributes: [String: Any?] = [ - "d_4_2": -9007199254740994 + "d_4_2": Double(-9007199254740994) ] let variationKey = try? optimizely.activate(experimentKey: experimentKey, userId: userId, attributes: attributes) @@ -79,7 +79,7 @@ class OptimizelyClientTests_Evaluation: XCTestCase { let userId = "test_user_1" let attributes: [String: Any?] = [ - "d_4_2": 9007199254740994 + "d_4_2": Double(9007199254740994) ] let variationKey = try? optimizely.activate(experimentKey: experimentKey, userId: userId, attributes: attributes) diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift new file mode 100644 index 00000000..85b9c0f6 --- /dev/null +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift @@ -0,0 +1,231 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OptimizelyClientTests_ODP: XCTestCase { + + var optimizely: OptimizelyClient! + + override func setUp() { + super.setUp() + + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + try! optimizely.start(datafile: datafile) + } + + // MARK: - ODP configuration + + func testConfigurableSettings_default() { + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + + XCTAssertEqual(100, optimizely.odpManager.segmentManager?.segmentsCache.maxSize) + XCTAssertEqual(600, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) + XCTAssertEqual(true, optimizely.odpManager.enabled) + } + + func testConfigurableSettings_custom() { + var sdkSettings = OptimizelySdkSettings(segmentsCacheSize: 12, segmentsCacheTimeoutInSecs: 345) + var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) + XCTAssertEqual(12, optimizely.odpManager.segmentManager?.segmentsCache.maxSize) + XCTAssertEqual(345, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) + + sdkSettings = OptimizelySdkSettings(disableOdp: true) + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) + XCTAssertEqual(false, optimizely.odpManager.enabled) + } + + // MARK: - sendOdpEvent + + func testSendOdpEvent_success() { + let odpManager = MockOdpManager(sdkKey: "any", disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) + optimizely.odpManager = odpManager + + try? optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k21": "v2", "k22": true, "k23": 3.5, "k24": nil]) + + XCTAssertEqual("t1", odpManager.eventType) + XCTAssertEqual("a1", odpManager.eventAction) + XCTAssertEqual(["k1": "v1"], odpManager.eventIdentifiers) + XCTAssertEqual("v2", odpManager.eventData!["k21"] as! String) + XCTAssertEqual(true, odpManager.eventData!["k22"] as! Bool) + XCTAssertEqual(3.5, odpManager.eventData!["k23"] as! Double) + // swift handles in Any type in a weird way. It's a nil but cannot be AssertNil. Use stringify to validate nil. + XCTAssertNil(odpManager.eventData!["k24"]!) + + // default event props + + try? optimizely.sendOdpEvent(action: "a2") + + XCTAssertEqual("fullstack", odpManager.eventType) + XCTAssertEqual("a2", odpManager.eventAction) + XCTAssertEqual([:], odpManager.eventIdentifiers) + XCTAssertEqual([:], odpManager.eventData as! [String: String]) + } + + func testSendOdpEvent_error() { + var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + + do { + try optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k2": "v2"]) + XCTAssert(true, "event must be queued if datafile is not ready") + } catch { + XCTFail() + } + + var datafile = OTUtils.loadJSONDatafile("empty_datafile")! + try! optimizely.start(datafile: datafile) + + do { + try optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k2": "v2"]) + XCTFail() + } catch OptimizelyError.odpNotIntegrated { + XCTAssert(true, "OptimizelyError expected if ODP is not integrated.") + } catch { + XCTFail() + } + + datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + try! optimizely.start(datafile: datafile) + + do { + try optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k2": "v2"]) + XCTAssert(true) + } catch { + XCTFail() + } + + let sdkSettings = OptimizelySdkSettings(disableOdp: true) + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) + datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + try! optimizely.start(datafile: datafile) + + do { + try optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k2": "v2"]) + XCTAssert(true) + } catch OptimizelyError.odpNotEnabled { + XCTAssert(true, "OptimizelyError expected if ODP is disabled.") + } catch { + XCTFail() + } + } + + func testSendOdpEvent_invalidDataTypes() { + do { + try optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k21": "valid", "k22": ["invalid"]]) + XCTFail() + } catch OptimizelyError.odpInvalidData { + XCTAssert(true) + } catch { + XCTFail("OptimizelyError expected if data has invalid types.") + } + + do { + try optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k2": ["embed": 12]]) + XCTFail() + } catch OptimizelyError.odpInvalidData { + XCTAssert(true) + } catch { + XCTFail("OptimizelyError expected if data has invalid types.") + } + + do { + try optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: [:], + data: ["k1": "v1", + "k2": true, + "k3": 3.5, + "k4": 10, + "k5": nil + ]) + + XCTAssert(true) + } catch { + XCTFail("Should accept all valid data value types.") + } + } + + // MARK: - vuid + + func testVuid() { + XCTAssert(optimizely.vuid.starts(with: "vuid_")) + } + + // MARK: - OdpConfig Update + + func testUpdateOpdConfigCalled_wheneverProjectConfigUpdated_initialOrPolling() { + let odpManager = MockOdpManager(sdkKey: "any", disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) + optimizely.odpManager = odpManager + + XCTAssertNil(odpManager.apiKey) + XCTAssertNil(odpManager.apiHost) + XCTAssertEqual([], odpManager.segmentsToCheck) + + // ODP integrated in datafile + + var datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + updateProjectConfig(optimizely: optimizely, datafile: datafile) + + XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", odpManager.apiKey, "updateOdpConfig should be called when datafile parsed ok") + XCTAssertEqual("https://api.zaius.com", odpManager.apiHost) + XCTAssertEqual(["odp-segment-1", "odp-segment-2", "odp-segment-3"], odpManager.segmentsToCheck.sorted()) + + // ODP not integrated in datafile + + datafile = OTUtils.loadJSONDatafile("decide_datafile")! + updateProjectConfig(optimizely: optimizely, datafile: datafile) + + XCTAssertNil(odpManager.apiKey, "updateOdpConfig should be called when datafile parsed ok, but no odp integrated") + XCTAssertNil(odpManager.apiHost) + XCTAssertEqual([], odpManager.segmentsToCheck) + } + + // MARK: - Utils + + func updateProjectConfig(optimizely: OptimizelyClient, datafile: Data) { + optimizely.config = try! ProjectConfig(datafile: datafile) + } + +} + +// MARK: - Mocks + +extension OptimizelyClientTests_ODP { + + class MockOdpManager: OdpManager { + var eventType: String? + var eventAction: String? + var eventIdentifiers: [String: String]? + var eventData: [String: Any?]? + + var apiKey: String? + var apiHost: String? + var segmentsToCheck = [String]() + + override func sendEvent(type: String, action: String, identifiers: [String : String], data: [String : Any?]) { + self.eventType = type + self.eventAction = action + self.eventIdentifiers = identifiers + self.eventData = data + } + + override func updateOdpConfig(apiKey: String?, apiHost: String?, segmentsToCheck: [String]) { + self.apiKey = apiKey + self.apiHost = apiHost + self.segmentsToCheck = segmentsToCheck + } + } + +} diff --git a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Attributes.swift b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Attributes.swift index 206f5216..3b9865b4 100644 --- a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Attributes.swift +++ b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Attributes.swift @@ -18,7 +18,7 @@ import XCTest class BatchEventBuilderTests_Attributes: XCTestCase { - let botFilteringKey = Constants.Attributes.OptimizelyBotFilteringAttribute + let botFilteringKey = Constants.Attributes.reservedBotFilteringAttribute let experimentKey = "ab_running_exp_audience_combo_exact_foo_or_true__and__42_or_4_2" let userId = "test_user_1" diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index dc0b7a28..2dd574d2 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift @@ -1213,7 +1213,8 @@ class FakeManager: OptimizelyClient { datafileHandler: OPTDatafileHandler? = nil, userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil) { + defaultDecideOptions: [OptimizelyDecideOption]? = nil, + settings: OptimizelySdkSettings? = nil) { // clear shared handlers HandlerRegistryService.shared.removeAll() @@ -1224,7 +1225,8 @@ class FakeManager: OptimizelyClient { datafileHandler: datafileHandler, userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions) + defaultDecideOptions: defaultDecideOptions, + settings: settings) let userProfileService = userProfileService ?? DefaultUserProfileService() self.decisionService = FakeDecisionService(userProfileService: userProfileService) diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift index a84045f2..fff345ea 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift @@ -349,22 +349,19 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryMatch)).result XCTAssert(result, "attribute should be matched to audienceConditions") // (2) matching false result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryNotMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryNotMatch)).result XCTAssertFalse(result, "attribute should be matched to audienceConditions") // (3) other attribute result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssertFalse(result, "no matching attribute provided") } @@ -380,22 +377,19 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryMatch)).result XCTAssert(result, "attribute should be matched to audienceConditions") // (2) matching false result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryNotMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryNotMatch)).result XCTAssertFalse(result, "attribute should be matched to audienceConditions") // (3) other attribute result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssertFalse(result, "no matching attribute provided") } @@ -408,8 +402,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryMatch)).result XCTAssert(result, "empty conditions is true always") } @@ -422,8 +415,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryMatch)).result XCTAssert(result, "empty conditions is true always") } @@ -441,14 +433,12 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryMatch)).result XCTAssert(result) result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesEmpty).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesEmpty)).result XCTAssertFalse(result) // (2) invalid string in "audienceConditions" @@ -458,8 +448,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryMatch)).result XCTAssert(result) // (2) invalid string in "audienceConditions" @@ -469,8 +458,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryMatch)).result XCTAssert(result) } @@ -490,8 +478,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -507,8 +494,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -524,8 +510,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -541,8 +526,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesCountryMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesCountryMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -558,8 +542,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -575,8 +558,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: ["age": nil]).result + user: OTUtils.user(userId: kUserId, attributes: ["age": nil])).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -592,8 +574,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -609,8 +590,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: ["country": ["invalid"]]).result + user: OTUtils.user(userId: kUserId, attributes: ["country": ["invalid"]])).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -626,8 +606,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: ["age": Double.infinity]).result + user: OTUtils.user(userId: kUserId, attributes: ["age": Double.infinity])).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -643,8 +622,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -660,8 +638,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: ["age": ["invalid"]]).result + user: OTUtils.user(userId: kUserId, attributes: ["age": ["invalid"]])).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -677,8 +654,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -694,8 +670,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: ["age": ["invalid"]]).result + user: OTUtils.user(userId: kUserId, attributes: ["age": ["invalid"]])).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -711,8 +686,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: kAttributesAgeMatch).result + user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) @@ -728,9 +702,7 @@ extension DecisionServiceTests_Experiments { result = self.decisionService.doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: ["age": ["invalid"]]).result - + user: OTUtils.user(userId: kUserId, attributes: ["age": ["invalid"]])).result XCTAssert(MockLogger.logFound) XCTAssertFalse(result) } diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_Others.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_Others.swift index 63e75cc3..0fc55438 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_Others.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_Others.swift @@ -36,8 +36,7 @@ class DecisionServiceTests_Others: XCTestCase { let isValid = (optimizely.decisionService as! DefaultDecisionService) .doesMeetAudienceConditions(config: config, experiment: experiment, - userId: kUserId, - attributes: attributes).result! + user: OTUtils.user(userId: kUserId, attributes: attributes)).result! XCTAssert(isValid) } diff --git a/Tests/OptimizelyTests-Common/LruCacheTests.swift b/Tests/OptimizelyTests-Common/LruCacheTests.swift new file mode 100644 index 00000000..ddcfa299 --- /dev/null +++ b/Tests/OptimizelyTests-Common/LruCacheTests.swift @@ -0,0 +1,174 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class LruCacheTests: XCTestCase { + + func testMinConfig() { + var cache = LruCache(size: 1000, timeoutInSecs: 2000) + XCTAssertEqual(1000, cache.maxSize) + XCTAssertEqual(2000, cache.timeoutInSecs) + + cache = LruCache(size: 0, timeoutInSecs: 0) + XCTAssertEqual(0, cache.maxSize) + XCTAssertEqual(0, cache.timeoutInSecs) + } + + func testSaveAndLookup() { + let maxSize = 2 + let cache = LruCache(size: maxSize, timeoutInSecs: 1000) + + XCTAssertNil(cache.peek(key: 1)) + cache.save(key: 1, value: 100) // [1] + cache.save(key: 2, value: 200) // [1, 2] + cache.save(key: 3, value: 300) // [2, 3] + XCTAssertNil(cache.peek(key: 1)) + XCTAssertEqual(200, cache.peek(key: 2)) + XCTAssertEqual(300, cache.peek(key: 3)) + + cache.save(key: 2, value: 201) // [3, 2] + cache.save(key: 1, value: 101) // [2, 1] + XCTAssertEqual(101, cache.peek(key: 1)) + XCTAssertEqual(201, cache.peek(key: 2)) + XCTAssertNil(cache.peek(key: 3)) + + XCTAssertNil(cache.lookup(key: 3)) // [2, 1] + XCTAssertEqual(201, cache.lookup(key: 2)) // [1, 2] + cache.save(key: 3, value: 302) // [2, 3] + XCTAssertNil(cache.peek(key: 1)) + XCTAssertEqual(201, cache.peek(key: 2)) + XCTAssertEqual(302, cache.peek(key: 3)) + + XCTAssertEqual(302, cache.lookup(key: 3)) // [2, 3] + cache.save(key: 1, value: 103) // [3, 1] + XCTAssertEqual(103, cache.peek(key: 1)) + XCTAssertNil(cache.peek(key: 2)) + XCTAssertEqual(302, cache.peek(key: 3)) + + var node: LruCache.CacheElement? = cache.head + var count = 0 + while node != nil { + count += 1 + node = node?.next + } + XCTAssertEqual(maxSize, count - 2) // subtract 2 (head, tail) + XCTAssertEqual(cache.map.count, cache.maxSize) + } + + func testReset() { + let maxSize = 2 + let cache = LruCache(size: maxSize, timeoutInSecs: 1000) + + cache.save(key: 1, value: 100) // [1] + cache.save(key: 2, value: 200) // [1, 2] + sleep(1) + + XCTAssertEqual(cache.map.count, 2) + + // cache reset + + cache.reset() + sleep(1) + + XCTAssertEqual(cache.map.count, 0) + + // validate cache fully functional after reset + + cache.save(key: 3, value: 300) // [3] + cache.save(key: 2, value: 400) // [3, 2] + + XCTAssertNil(cache.peek(key: 1)) + XCTAssertEqual(400, cache.peek(key: 2)) + XCTAssertEqual(300, cache.peek(key: 3)) + + cache.save(key: 3, value: 600) // [2, 3] + cache.save(key: 1, value: 500) // [3, 1] + XCTAssertNil(cache.peek(key: 2)) + XCTAssertEqual(600, cache.peek(key: 3)) + XCTAssertEqual(500, cache.peek(key: 1)) + + _ = cache.lookup(key: 3) // [1, 3] + cache.save(key: 2, value: 700) // [3, 2] + XCTAssertNil(cache.peek(key: 1)) + XCTAssertEqual(600, cache.peek(key: 3)) + XCTAssertEqual(700, cache.peek(key: 2)) + } + + func testSize_zero() { + let cache = LruCache(size: 0, timeoutInSecs: 1000) + + XCTAssertNil(cache.lookup(key: 1)) + cache.save(key: 1, value: 100) // [1] + XCTAssertNil(cache.lookup(key: 1)) + cache.reset() + XCTAssertNil(cache.lookup(key: 1)) + } + + func testThreadSafe() { + let numThreads = 100 + let numIterationPerThread = 1000 + + let cache = LruCache(size: 3, timeoutInSecs: 1) + let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 10) { idx in + for i in 0..(size: 1000, timeoutInSecs: maxTimeout) + + cache.save(key: 1, value: 100) // [1] + cache.save(key: 2, value: 200) // [1, 2] + cache.save(key: 3, value: 300) // [1, 2, 3] + sleep(2) // wait to expire + cache.save(key: 4, value: 400) // [1, 2, 3] + cache.save(key: 1, value: 101) // [1] + + XCTAssertEqual(101, cache.lookup(key: 1)) + XCTAssertNil(cache.lookup(key: 2)) + XCTAssertNil(cache.lookup(key: 3)) + XCTAssertEqual(400, cache.lookup(key: 4)) + } + + func testTimeout_zero() { + let maxTimeout = 0 + let cache = LruCache(size: 1000, timeoutInSecs: maxTimeout) + + cache.save(key: 1, value: 100) // [1] + cache.save(key: 2, value: 200) // [1, 2] + sleep(2) // wait to expire + + XCTAssertEqual(100, cache.lookup(key: 1), "should not expire when timeout is 0") + XCTAssertEqual(200, cache.lookup(key: 2)) + } + +} + +#endif diff --git a/Tests/OptimizelyTests-Common/OdpEventApiManagerTests.swift b/Tests/OptimizelyTests-Common/OdpEventApiManagerTests.swift new file mode 100644 index 00000000..9609c626 --- /dev/null +++ b/Tests/OptimizelyTests-Common/OdpEventApiManagerTests.swift @@ -0,0 +1,261 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OdpEventApiManagerTests: XCTestCase { + let userKey = "vuid" + let userValue = "test-user-value" + let apiKey = "test-api-key" + let apiHost = "test-host" + + let events: [OdpEvent] = [ + OdpEvent(type: "t1", action: "a1", identifiers: ["id-key-1": "id-value-1"], data: ["key11": "value-1", "key12": true, "key13": 3.5]), + OdpEvent(type: "t2", action: "a2", identifiers: ["id-key-2": "id-value-2"], data: ["key2": "value-2"]) + ] + + // MARK: - success + + func testSendOdpEvents_validRequest() { + let session = MockOdpUrlSession(statusCode: 200, responseData: MockOdpUrlSession.successResponseData) + let api = MockOdpEventApiManager(session) + + let sem = DispatchSemaphore(value: 0) + api.sendOdpEvents(apiKey: apiKey, apiHost: apiHost, events: events) { _ in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + let request = session.receivedApiRequest! + + XCTAssertEqual(apiHost + "/v3/events", request.url?.absoluteString) + XCTAssertEqual("POST", request.httpMethod) + XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) + XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) + + let bodyArray = try! JSONSerialization.jsonObject(with: request.httpBody!, options: []) as! [[String: Any]] + let expectedArray = events.map { $0.dict } + XCTAssertEqual(2, bodyArray.count) + for i in 0.. data value is converted to NSNull () after saving into and retrieving from the event queue. + // - validate both for nil and NSNull + OdpEvent(type: "t1", action: "a1", identifiers: ["id1": "value1"], data: ["key1": "value1", "key2": nil, "key3": NSNull()]), + ] + + let session = MockOdpUrlSession(statusCode: 200, responseData: MockOdpUrlSession.successResponseData) + let api = MockOdpEventApiManager(session) + + let sem = DispatchSemaphore(value: 0) + api.sendOdpEvents(apiKey: apiKey, apiHost: apiHost, events: events) { _ in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + let request = session.receivedApiRequest! + + let jsonDispatched = String(bytes: request.httpBody!, encoding: .utf8)! + // [{"data":{"key2":null,"key3":null,"key1":"value1"},"type":"t1","identifiers":{"id1":"value1"},"action":"a1"}] + XCTAssert(jsonDispatched.contains("\"key2\":null")) + XCTAssert(jsonDispatched.contains("\"key3\":null")) + } + +} + +// MARK: - Tests with live ODP server +// tests below will be skipped in CI (travis/actions) since they use the live ODP server. +#if DEBUG + +extension OdpEventApiManagerTests { + + var odpApiKey: String { return "W4WzcEs-ABgXorzY7h1LCQ" } + var odpApiHost: String { return "https://api.zaius.com" } + var odpValidUserId: String { return "tester-101"} + + func testLiveOdpRest() { + let manager = OdpEventApiManager() + + let sem = DispatchSemaphore(value: 0) + manager.sendOdpEvents(apiKey: odpApiKey, + apiHost: odpApiHost, + events: [OdpEvent(type: "t1", + action: "a1", + identifiers: ["vuid": "idv1"], + data: ["key1": "value1", "key2": 3.5, "key3": false, "key4": nil])]) { error in + XCTAssertNil(error) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + func testLiveOdpRest_error() { + let manager = OdpEventApiManager() + + let sem = DispatchSemaphore(value: 0) + manager.sendOdpEvents(apiKey: odpApiKey, + apiHost: odpApiHost, + events: [OdpEvent(type: "t1", + action: "a1", + identifiers: [:], + data: [:])]) { error in + XCTAssertNotNil(error, "empty identifiers not allowed in ODP") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } +} + +#endif + +// MARK: - MockOdpEventApiManager + +extension OdpEventApiManagerTests { + + class MockOdpEventApiManager: OdpEventApiManager { + let mockUrlSession: URLSession + + init(_ urlSession: URLSession) { + mockUrlSession = urlSession + } + + override func getSession() -> URLSession { + return mockUrlSession + } + } + + class MockOdpUrlSession: URLSession { + static var validSessions = 0 + var statusCode: Int + var withError: Bool + var responseData: String? + var receivedApiRequest: URLRequest? + + class MockDataTask: URLSessionDataTask { + var task: () -> Void + + init(_ task: @escaping () -> Void) { + self.task = task + } + + override func resume() { + task() + } + } + + init(statusCode: Int = 0, withError: Bool = false, responseData: String? = nil) { + Self.validSessions += 1 + self.statusCode = statusCode + self.withError = withError + self.responseData = responseData ?? MockOdpUrlSession.successResponseData + } + + override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + receivedApiRequest = request + + return MockDataTask() { + let statusCode = self.statusCode != 0 ? self.statusCode : 200 + let response = HTTPURLResponse(url: request.url!, + statusCode: statusCode, + httpVersion: nil, + headerFields: [String: String]()) + + let data = self.responseData?.data(using: .utf8) + let error = self.withError ? OptimizelyError.generic : nil + + completionHandler(data, response, error) + } + } + + override func finishTasksAndInvalidate() { + Self.validSessions -= 1 + } + + // MARK: - Utils + + static let successResponseData: String = """ + {"title":"Accepted","status":202,"timestamp":"2022-07-01T16:04:06.786Z"} + """ + + static let failureResponseData: String = """ + {"title":"Bad Request","status":400,"timestamp":"2022-07-01T20:44:00.945Z","detail":{"invalids":[{"event":0,"message":"missing 'type' field"}]}} + """ + } +} diff --git a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift new file mode 100644 index 00000000..1776945d --- /dev/null +++ b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift @@ -0,0 +1,484 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OdpEventManagerTests: XCTestCase { + var manager: OdpEventManager! + var odpConfig: OdpConfig! + var apiManager = MockOdpEventApiManager() + + var options = [OptimizelySegmentOption]() + + var userKey = "vuid" + var userValue = "test-user" + let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + let customData: [String: Any] = ["key-1": "value-1", + "key-2": 12.5, + "model": "overruled"] + + override func setUp() { + OTUtils.clearAllEventQueues() + OTUtils.createDocumentDirectoryIfNotAvailable() + + // no valid apiKey, so flush will return immediately + odpConfig = OdpConfig() + + manager = OdpEventManager(sdkKey: "any", + odpConfig: odpConfig, + apiManager: apiManager) + } + + override func tearDown() { + OTUtils.clearAllEventQueues() + } + + // MARK: - save and restore events + + func testSaveAndRestoreEvents() { + manager.sendEvent(type: "t1", + action: "a1", + identifiers: ["id-key-1": "id-value-1"], + data: ["key1": "value1", "key2": 3.5, "key3": true, "key4": nil]) + + let evt = manager.eventQueue.getFirstItems(count: 1)!.first! + XCTAssertEqual("t1", evt.type) + XCTAssertEqual("a1", evt.action) + XCTAssertEqual(["id-key-1": "id-value-1"], evt.identifiers) + XCTAssertEqual("value1", evt.data["key1"] as! String) + XCTAssertEqual(3.5, evt.data["key2"] as! Double) + XCTAssertEqual(true, evt.data["key3"] as! Bool) + // data value is converted to NSNull () after saving into and retrieving from the event queue. + XCTAssert(evt.data["key4"] is NSNull) + } + + // MARK: - sendEvent + + func testSendEvent_noApiKey() { + manager.sendEvent(type: "t1", + action: "a1", + identifiers: ["id-key-1": "id-value-1"], + data: customData) + + XCTAssertEqual(1, manager.eventQueue.count) + sleep(1) + XCTAssertEqual(1, manager.eventQueue.count, "not flushed since apiKey is not ready") + + let evt = manager.eventQueue.getFirstItem()! + + XCTAssertEqual("t1", evt.type) + XCTAssertEqual("a1", evt.action) + XCTAssertEqual(["id-key-1": "id-value-1"], evt.identifiers) + validateData(evt.data, customData: customData) + } + + func testRegisterVUID_noApiKey() { + manager.registerVUID(vuid: "v1") + + XCTAssertEqual(1, manager.eventQueue.count) + + let evt = manager.eventQueue.getFirstItem()! + print("[ODP event default data] ", evt.data) + + XCTAssertEqual("fullstack", evt.type) + XCTAssertEqual("client_initialized", evt.action) + XCTAssertEqual(["vuid": "v1"], evt.identifiers) + validateData(evt.data, customData: [:]) + } + + func testIdentifyUser_noApiKey() { + manager.identifyUser(vuid: "v1", userId: "u1") + + XCTAssertEqual(1, manager.eventQueue.count) + let evt = manager.eventQueue.getFirstItem()! + XCTAssertEqual("fullstack", evt.type) + XCTAssertEqual("identified", evt.action) + XCTAssertEqual(["vuid": "v1", "fs_user_id": "u1"], evt.identifiers) + validateData(evt.data, customData: [:]) + } + + func testSendEvent_apiKey() { + odpConfig = OdpConfig() + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + + manager = OdpEventManager(sdkKey: "any", + odpConfig: odpConfig, + apiManager: apiManager) + manager.sendEvent(type: "t1", + action: "a1", + identifiers: ["id-key-1": "id-value-1"], + data: customData) + + XCTAssertEqual(1, manager.eventQueue.count) + sleep(1) + XCTAssertEqual(0, manager.eventQueue.count, "flushed since apiKey is ready") + } + + // MARK: - flush + + func testFlush_odpIntegrated() { + // apiKey is not ready initially + + XCTAssertTrue(manager.odpConfig.eventQueueingAllowed, "initially datafile not ready and assumed queueing is allowed") + + manager.registerVUID(vuid: "v1") // each of these will try to flush + manager.identifyUser(vuid: "v1", userId: "u1") + manager.sendEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + + XCTAssertEqual(3, manager.eventQueue.count) + sleep(1) + XCTAssertEqual(3, manager.eventQueue.count, "not flushed since apiKey is not ready") + + // apiKey is available in datafile (so ODP integrated) + + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + XCTAssertTrue(manager.odpConfig.eventQueueingAllowed, "datafile ready and odp integrated. event queueing is allowed.") + manager.flush() // need manual flush here since OdpManager is not connected + + sleep(1) + XCTAssertEqual(0, manager.eventQueue.count) + XCTAssertEqual(3, apiManager.totalDispatchedEvents) + apiManager.dispatchedBatchEvents.removeAll() + + // new events should be dispatched immediately + + manager.dispatch(event) // each of these will try to flush + manager.dispatch(event) // each of these will try to flush + + XCTAssertEqual(2, manager.eventQueue.count) + sleep(1) + XCTAssertEqual(0, manager.eventQueue.count, "auto flushed since apiKey is ready") + XCTAssertEqual(2, apiManager.totalDispatchedEvents) + } + + func testFlush_odpNotIntegrated() { + // apiKey is not ready + + XCTAssertTrue(manager.odpConfig.eventQueueingAllowed, "initially datafile not ready and assumed queueing is allowed") + + manager.registerVUID(vuid: "v1") // each of these will try to flush + manager.identifyUser(vuid: "v1", userId: "u1") + manager.sendEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + + XCTAssertEqual(3, manager.eventQueue.count) + sleep(1) + XCTAssertEqual(3, manager.eventQueue.count, "not flushed since apiKey is not ready") + + // apiKey is not available in datafile (so ODP not integrated) + + _ = odpConfig.update(apiKey: nil, apiHost: nil, segmentsToCheck: []) + XCTAssertFalse(manager.odpConfig.eventQueueingAllowed, "datafile ready and odp not integrated. event queueing is not allowed.") + + manager.flush() // need manual flush here since OdpManager is not connected + XCTAssertEqual(0, manager.eventQueue.count, "all old events are discarded since event queueing not allowed") + XCTAssertEqual(0, apiManager.totalDispatchedEvents, "all events discarded") + + manager.dispatch(event) // each of these will try to flush + manager.dispatch(event) // each of these will try to flush + + XCTAssertEqual(0, manager.eventQueue.count) + sleep(1) + XCTAssertEqual(0, manager.eventQueue.count, "all news events are discarded since event queueing not allowed") + XCTAssertEqual(0, apiManager.totalDispatchedEvents, "all events discarded") + } + + // MARK: - queue overflow + + func testFlush_maxSize() { + manager.maxQueueSize = 2 + + manager.registerVUID(vuid: "v1") // each of these will try to flush + manager.identifyUser(vuid: "v1", userId: "u1") + manager.sendEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + + sleep(1) + XCTAssertEqual(2, manager.eventQueue.count, "an event discarded since queue overflowed") + + // apiKey is available in datafile (so ODP integrated) + + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + + manager.dispatch(event) // each of these will try to flush + + sleep(1) + XCTAssertEqual(0, manager.eventQueue.count, "flush is called even when an event is discarded because queue is overflowed") + XCTAssertEqual(2, apiManager.totalDispatchedEvents) + } + + // MARK: - batch + + func testFlush_batch_1() { + let events = [ + OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + ] + manager.dispatch(events[0]) + + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + manager.flush() + sleep(1) + + XCTAssertEqual(1, apiManager.dispatchedBatchEvents.count) + XCTAssertEqual(1, apiManager.dispatchedBatchEvents[0].count) + validateEvents(events, apiManager.dispatchedBatchEvents[0]) + } + + func testFlush_batch_3() { + let events = [ + OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]), + OdpEvent(type: "t2", action: "a2", identifiers: [:], data: [:]), + OdpEvent(type: "t3", action: "a3", identifiers: [:], data: [:]) + ] + + for e in events { + manager.dispatch(e) + } + + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + manager.flush() + sleep(1) + + XCTAssertEqual(1, apiManager.dispatchedBatchEvents.count) + XCTAssertEqual(3, apiManager.dispatchedBatchEvents[0].count) + validateEvents(events, apiManager.dispatchedBatchEvents[0]) + } + + func testFlush_batch_moreThanBatchSize() { + let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + let events = [OdpEvent](repeating: event, count: 11) + + for e in events { + manager.dispatch(e) + } + + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + manager.flush() + sleep(1) + + XCTAssertEqual(2, apiManager.dispatchedBatchEvents.count) + XCTAssertEqual(10, apiManager.dispatchedBatchEvents[0].count) + XCTAssertEqual(1, apiManager.dispatchedBatchEvents[1].count) + validateEvents(events, apiManager.dispatchedBatchEvents[0] + apiManager.dispatchedBatchEvents[1]) + } + + func testFlush_emptyQueue() { + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + manager.flush() + sleep(1) + + XCTAssertEqual(0, apiManager.dispatchedBatchEvents.count) + } + + // MARK: - multiple skdKeys + + func testMultipleSdkKeys_doNotInterfere() { + let apiManager1 = MockOdpEventApiManager() + let apiManager2 = MockOdpEventApiManager() + let odpConfig1 = OdpConfig() + let odpConfig2 = OdpConfig() + + let manager1 = OdpEventManager(sdkKey: "sdkKey-1", + odpConfig: odpConfig1, + apiManager: apiManager1) + let manager2 = OdpEventManager(sdkKey: "sdkKey-2", + odpConfig: odpConfig2, + apiManager: apiManager2) + + let event1 = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + let event2 = OdpEvent(type: "t2", action: "a2", identifiers: [:], data: [:]) + + manager1.dispatch(event1) + manager1.dispatch(event1) + + manager2.dispatch(event2) + + XCTAssertEqual(0, apiManager1.dispatchedBatchEvents.count) + XCTAssertEqual(0, apiManager2.dispatchedBatchEvents.count) + + _ = odpConfig1.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + manager1.flush() + sleep(1) + + XCTAssertEqual(1, apiManager1.dispatchedBatchEvents.count) + XCTAssertEqual(2, apiManager1.dispatchedBatchEvents[0].count) + XCTAssertEqual(0, apiManager2.dispatchedBatchEvents.count) + + _ = odpConfig2.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + manager2.flush() + sleep(1) + + XCTAssertEqual(1, apiManager1.dispatchedBatchEvents.count) + XCTAssertEqual(2, apiManager1.dispatchedBatchEvents[0].count) + XCTAssertEqual(1, apiManager2.dispatchedBatchEvents.count) + XCTAssertEqual(1, apiManager2.dispatchedBatchEvents[0].count) + } + + // MARK: - errors + + func testFlushError_retry() { + let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + let events = [OdpEvent](repeating: event, count: 2) + + for e in events { + manager.dispatch(e) + } + + _ = odpConfig.update(apiKey: "valid-key-retry-error", apiHost: "host", segmentsToCheck: []) + manager.flush() + sleep(1) + + XCTAssertEqual(1, apiManager.dispatchedBatchEvents.count, "should be not retried immediately (a batch of 2 events)") + XCTAssertEqual(2, manager.eventQueue.count, "the events should remain in the queue after giving up for later retries") + } + + func testFlushError_noRetry() { + let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + let events = [OdpEvent](repeating: event, count: 15) + + for e in events { + manager.dispatch(e) + } + + _ = odpConfig.update(apiKey: "invalid-key-no-retry", apiHost: "host", segmentsToCheck: []) + manager.flush() + sleep(1) + + XCTAssertEqual(2, apiManager.dispatchedBatchEvents.count, "should not be retried (only once for each of batch events)") + XCTAssertEqual(10, apiManager.dispatchedBatchEvents[0].count) + XCTAssertEqual(5, apiManager.dispatchedBatchEvents[1].count) + XCTAssertEqual(0, manager.eventQueue.count, "all the events should be discarded") + } + + func testReset() { + let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + let events = [OdpEvent](repeating: event, count: 3) + + manager.reset() + XCTAssertEqual(0, manager.eventQueue.count) + + for e in events { + manager.dispatch(e) + } + + XCTAssertEqual(3, manager.eventQueue.count) + + manager.reset() + XCTAssertEqual(0, manager.eventQueue.count) + } + + // MARK: - OdpConfig + + func testOdpConfig() { + _ = odpConfig.update(apiKey: "test-key", apiHost: "test-host", segmentsToCheck: []) + + manager = OdpEventManager(sdkKey: "any", + odpConfig: odpConfig, + apiManager: apiManager) + + let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + manager.dispatch(event) + sleep(1) + + XCTAssertEqual("test-host", apiManager.receivedApiHost) + XCTAssertEqual("test-key", apiManager.receivedApiKey) + } + + // MARK: - Utils + + func validateData(_ data: [String: Any?], customData: [String: Any?]) { + XCTAssert((data["idempotence_id"] as! String).count > 3) + XCTAssert((data["data_source_type"] as! String) == "sdk") + XCTAssert((data["data_source"] as! String) == "swift-sdk") + XCTAssert((data["data_source_version"] as! String).count > 3) + XCTAssert((data["os_version"] as! String).count > 3) + + // os-dependent + + let dataOS = data["os"] as! String + let dataDeviceType = data["device_type"] as! String + + #if os(iOS) + XCTAssertEqual(dataOS, "iOS") + if UIDevice.current.userInterfaceIdiom == .phone { + XCTAssertEqual(dataDeviceType, "Phone") + } else { + XCTAssertEqual(dataDeviceType, "Tablet") + } + #elseif os(tvOS) + XCTAssertEqual(dataOS, "tvOS") + XCTAssertEqual(dataDeviceType, "Smart TV") + #elseif os(watchOS) + XCTAssertEqual(dataOS, "watchOS") + XCTAssertEqual(dataDeviceType, "Watch") + #elseif os(macOS) + XCTAssertEqual(dataOS, "macOS") + XCTAssertEqual(dataDeviceType, "PC") + #else + XCTAssertEqual(dataOS, "Other") + XCTAssertEqual(dataDeviceType, "Other") + #endif + + // overruled ("model") or other custom data + + if customData.isEmpty { + XCTAssert((data["model"] as! String).count > 3) + XCTAssertNil(data["key-1"] as? String) + XCTAssertNil(data["key-2"] as? String) + } else { + XCTAssert((data["model"] as! String) == "overruled") + XCTAssert((data["key-1"] as! String) == "value-1") + XCTAssert((data["key-2"] as! Double) == 12.5) + } + } + + func validateEvents(_ lhs: [OdpEvent], _ rhs: [OdpEvent]) { + XCTAssertEqual(lhs.count, rhs.count) + for i in 0.. Void) { + receivedApiKey = apiKey + receivedApiHost = apiHost + dispatchedBatchEvents.append(events) + + DispatchQueue.global().async { + if apiKey == "invalid-key-no-retry" { + completionHandler(OptimizelyError.odpEventFailed("403", false)) + } else if apiKey == "valid-key-retry-error" { + completionHandler(OptimizelyError.odpEventFailed("network error", true)) + } else { + completionHandler(nil) + } + } + } + } + +} diff --git a/Tests/OptimizelyTests-Common/OdpManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift new file mode 100644 index 00000000..1d74e7fe --- /dev/null +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -0,0 +1,376 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OdpManagerTests: XCTestCase { + let sdkKey = "any" + let cacheSize = 10 + let cacheTimeout = 20 + var segmentManager: MockOdpSegmentManager! + var eventManager: MockOdpEventManager! + var manager: OdpManager! + + override func setUp() { + OTUtils.clearAllEventQueues() + segmentManager = MockOdpSegmentManager(cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout) + eventManager = MockOdpEventManager(sdkKey: sdkKey) + manager = OdpManager(sdkKey: sdkKey, + disable: false, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout, + segmentManager: segmentManager, + eventManager: eventManager) + } + + override func tearDown() { + OTUtils.clearAllEventQueues() + } + + // MARK: - Configurables + + func testConfigurations_cache() { + let manager = OdpManager(sdkKey: sdkKey, + disable: false, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout) + XCTAssertEqual(manager.segmentManager?.segmentsCache.maxSize, cacheSize) + XCTAssertEqual(manager.segmentManager?.segmentsCache.timeoutInSecs, cacheTimeout) + } + + // MARK: - disable ODP + + func testConfigurations_disableOdp() { + let manager = OdpManager(sdkKey: sdkKey, + disable: true, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout) + + XCTAssertTrue(manager.vuid.starts(with: "vuid_"), "vuid should be serverved even when ODP is disabled.") + + let sem = DispatchSemaphore(value: 0) + manager.fetchQualifiedSegments(userId: "user1", options: []) { segments, error in + XCTAssertNil(segments) + XCTAssertEqual(error?.reason, OptimizelyError.odpNotEnabled.reason) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + manager.updateOdpConfig(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + XCTAssertNil(manager.odpConfig) + + // these calls should be dropped gracefully with nil + + manager.identifyUser(userId: "user1") + try? manager.sendEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + + XCTAssertNil(manager.eventManager) + XCTAssertNil(manager.segmentManager) + } + + // MARK: - fetchQualifiedSegments + + func testFetchQualifiedSegments() { + let vuid = "vuid_123" + manager.fetchQualifiedSegments(userId: vuid, options: [.ignoreCache]) { _, _ in } + + XCTAssertEqual(segmentManager.receivedUserKey, "vuid") + XCTAssertEqual(segmentManager.receivedUserValue, vuid) + XCTAssertEqual(segmentManager.receivedOptions, [.ignoreCache]) + + let userId = "user-1" + manager.fetchQualifiedSegments(userId: userId, options: []) { _, _ in } + + XCTAssertEqual(segmentManager.receivedUserKey, "fs_user_id") + XCTAssertEqual(segmentManager.receivedUserValue, "user-1") + XCTAssertEqual(segmentManager.receivedOptions, []) + } + + // MARK: - registerVuid + + func testRegisterVUIDCalledAutomatically() { + XCTAssertEqual(eventManager.receivedVuid, manager.vuid, "registerVUID is implicitly called on OdpManager init") + } + + func testRegisterVUIDCalledAutomatically_odpDisabled() { + let newEventManager = MockOdpEventManager(sdkKey: sdkKey, odpConfig: OdpConfig()) + + _ = OdpManager(sdkKey: sdkKey, + disable: true, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout, + segmentManager: segmentManager, + eventManager: newEventManager) + + XCTAssertNil(newEventManager.receivedVuid, "registerVUID should not implicitly called when ODP disabled") + } + + // MARK: - identifyUser + + func testIdentifyUser_datafileNotReady() { + manager.identifyUser(userId: "user-1") + + XCTAssertEqual(eventManager.receivedUserId, "user-1") + } + + func testIdentifyUser_odpIntegrated() { + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) + manager.identifyUser(userId: "user-1") + + XCTAssertEqual(eventManager.receivedUserId, "user-1") + } + + func testIdentifyUser_odpNotIntegrated() { + manager.updateOdpConfig(apiKey: nil, apiHost: nil, segmentsToCheck: []) + manager.identifyUser(userId: "user-1") + + XCTAssertNil(eventManager.receivedUserId, "identifyUser event requeut should be discarded if ODP not integrated.") + } + + func testIdentifyUser_odpDisabled() { + manager.enabled = false + manager.identifyUser(userId: "user-1") + + XCTAssertNil(eventManager.receivedUserId, "identifyUser event requeut should be discarded if ODP disabled.") + } + + // MARK: - sendEvent + + func testSendEvent_datafileNotReady() { + try? manager.sendEvent(type: "t1", action: "a1", identifiers: ["id-key1": "id-val-1"], data: ["key1" : "val1"]) + + XCTAssertEqual(eventManager.receivedType, "t1") + XCTAssertEqual(eventManager.receivedAction, "a1") + XCTAssertEqual(eventManager.receivedIdentifiers, ["vuid": manager.vuid,"id-key1": "id-val-1"]) + XCTAssert(eventManager.receivedData.count == 1) + XCTAssert((eventManager.receivedData["key1"] as! String) == "val1") + + // user-provided vuid should not be replaced + + try? manager.sendEvent(type: "t1", action: "a1", identifiers: ["vuid": "vuid-fixed", "id-key1": "id-val-1"], data: ["key1" : "val1"]) + + XCTAssertEqual(eventManager.receivedIdentifiers, ["vuid": "vuid-fixed", "id-key1": "id-val-1"]) + } + + func testSendEvent_odpIntegrated() { + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) + try? manager.sendEvent(type: "t1", action: "a1", identifiers: ["id-key1": "id-val-1"], data: ["key1" : "val1"]) + + XCTAssertEqual(eventManager.receivedType, "t1") + } + + func testSendEvent_odpNotIntegrated() { + manager.updateOdpConfig(apiKey: nil, apiHost: nil, segmentsToCheck: []) + try? manager.sendEvent(type: "t1", action: "a1", identifiers: ["id-key1": "id-val-1"], data: ["key1" : "val1"]) + + XCTAssertNil(eventManager.receivedType, "sendEvent requeut should be discarded if ODP not integrated.") + } + + func testSendEvent_odpDisabled() { + manager.enabled = false + try? manager.sendEvent(type: "t1", action: "a1", identifiers: ["id-key1": "id-val-1"], data: ["key1" : "val1"]) + + XCTAssertNil(eventManager.receivedType, "sendEvent requeut should be discarded if ODP disabled.") + } + + // MARK: - updateConfig + + func testUpdateOdpConfig_segmentResetCalled() { + // initially + // - apiKey = nil + // - apiHost = nil + // - segmentsToCheck = [] + + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) + XCTAssertTrue(segmentManager.resetCalled) + + segmentManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) + XCTAssertFalse(segmentManager.resetCalled, "no change, so reset should not be called") + + segmentManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-1", segmentsToCheck: []) + XCTAssertTrue(segmentManager.resetCalled) + + segmentManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-2", segmentsToCheck: []) + XCTAssertTrue(segmentManager.resetCalled) + + segmentManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-2", segmentsToCheck: ["a"]) + XCTAssertTrue(segmentManager.resetCalled) + + segmentManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-2", segmentsToCheck: ["a", "b"]) + XCTAssertTrue(segmentManager.resetCalled) + + segmentManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-2", segmentsToCheck: ["c"]) + XCTAssertTrue(segmentManager.resetCalled) + + segmentManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-2", segmentsToCheck: ["c"]) + XCTAssertFalse(segmentManager.resetCalled, "no change, so reset should not be called") + + segmentManager.resetCalled = false + + manager.updateOdpConfig(apiKey: nil, apiHost: nil, segmentsToCheck: []) + XCTAssertTrue(segmentManager.resetCalled) + } + + func testUpdateOdpConfig_flushCalled() { + // initially + // - apiKey = nil + // - apiHost = nil + // - segmentsToCheck = [] + + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) + XCTAssertEqual(eventManager.flushApiKeys.count, 1, "flush called before") + XCTAssertEqual(eventManager.flushApiKeys[0], nil) + + eventManager.flushApiKeys.removeAll() + eventManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-1", segmentsToCheck: []) + XCTAssertEqual(eventManager.flushApiKeys.count, 1) + XCTAssertEqual(eventManager.flushApiKeys[0], "key-1", "old events must be flushed with the old odp key") + + eventManager.flushApiKeys.removeAll() + eventManager.resetCalled = false + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-1", segmentsToCheck: []) + XCTAssertEqual(eventManager.flushApiKeys.count, 1) + XCTAssertEqual(eventManager.flushApiKeys[0], "key-2") + + eventManager.flushApiKeys.removeAll() + eventManager.resetCalled = false + + manager.updateOdpConfig(apiKey: nil, apiHost: nil, segmentsToCheck: []) + XCTAssertEqual(eventManager.flushApiKeys.count, 1) + XCTAssertEqual(eventManager.flushApiKeys[0], "key-2") + } + + func testUpdateOdpConfig_odpConfigPropagatedProperly() { + let manager = OdpManager(sdkKey: sdkKey, + disable: false, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout) + + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) + + XCTAssertEqual(manager.segmentManager?.odpConfig.apiKey, "key-1") + XCTAssertEqual(manager.segmentManager?.odpConfig.apiHost, "host-1") + XCTAssertEqual(manager.segmentManager?.odpConfig.eventQueueingAllowed, true) + XCTAssertEqual(manager.eventManager?.odpConfig.apiKey, "key-1") + XCTAssertEqual(manager.eventManager?.odpConfig.apiHost, "host-1") + XCTAssertEqual(manager.eventManager?.odpConfig.eventQueueingAllowed, true) + + // odp disabled with invalid apiKey (apiKey/apiHost propagated into submanagers) + + manager.updateOdpConfig(apiKey: nil, apiHost: nil, segmentsToCheck: []) + + XCTAssertEqual(manager.segmentManager?.odpConfig.apiKey, nil) + XCTAssertEqual(manager.segmentManager?.odpConfig.apiHost, nil) + XCTAssertEqual(manager.segmentManager?.odpConfig.eventQueueingAllowed, false) + XCTAssertEqual(manager.eventManager?.odpConfig.apiKey, nil) + XCTAssertEqual(manager.eventManager?.odpConfig.apiHost, nil) + XCTAssertEqual(manager.eventManager?.odpConfig.eventQueueingAllowed, false) + } + + // MARK: - flush on EnterBackground + + func testFlushWhenAppGoesToBackground() { + XCTAssertEqual(eventManager.flushApiKeys.count, 0) + manager.applicationDidEnterBackground() + XCTAssertEqual(eventManager.flushApiKeys.count, 1, "flush called when app goes to background") + } + + // MARK: - vuid + + func testVuid() { + XCTAssertEqual(manager.vuid, manager.vuidManager.vuid) + } + + // MARK: - Helpers + + class MockOdpEventManager: OdpEventManager { + var receivedVuid: String! + var receivedUserId: String! + + var receivedType: String! + var receivedAction: String! + var receivedIdentifiers: [String: String]! + var receivedData: [String: Any?]! + + var flushApiKeys = [String?]() + + var resetCalled = false + + override func registerVUID(vuid: String) { + self.receivedVuid = vuid + } + + override func identifyUser(vuid: String, userId: String) { + self.receivedVuid = vuid + self.receivedUserId = userId + } + + override func sendEvent(type: String, action: String, identifiers: [String: String], data: [String: Any?]) { + self.receivedType = type + self.receivedAction = action + self.receivedIdentifiers = identifiers + self.receivedData = data + } + + override func flush() { + self.flushApiKeys.append(odpConfig.apiKey) + } + + override func reset() { + self.resetCalled = true + } + } + + class MockOdpSegmentManager: OdpSegmentManager { + var receivedUserKey: String! + var receivedUserValue: String! + var receivedOptions: [OptimizelySegmentOption]! + + var resetCalled = false + + override func fetchQualifiedSegments(userKey: String, + userValue: String, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + self.receivedUserKey = userKey + self.receivedUserValue = userValue + self.receivedOptions = options + } + + override func reset() { + self.resetCalled = true + } + } + +} diff --git a/Tests/OptimizelyTests-Common/OdpSegmentApiManagerTests.swift b/Tests/OptimizelyTests-Common/OdpSegmentApiManagerTests.swift new file mode 100644 index 00000000..6d1aeb0b --- /dev/null +++ b/Tests/OptimizelyTests-Common/OdpSegmentApiManagerTests.swift @@ -0,0 +1,458 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OdpSegmentApiManagerTests: XCTestCase { + let userKey = "vuid" + let userValue = "test-user-value" + let apiKey = "test-api-key" + let apiHost = "test-host" + + static var createdApiRequest: URLRequest? + + // MARK: - Request + + func testFetchQualifiedSegments_validRequest() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(statusCode: 200, responseData: MockOdpUrlSession.goodResponseData)) + + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: ["a", "b", "c"]) {_,_ in } + + let request = OdpSegmentApiManagerTests.createdApiRequest! + let expectedBody: [String: Any] = [ + "query": "query($userId: String, $audiences: [String]) {customer(\(userKey): $userId) {audiences(subset: $audiences) {edges {node {name state}}}}}", + "variables": [ + "userId": userValue, + "audiences": ["a", "b", "c"] + ] + ] + + XCTAssertEqual(apiHost + "/v3/graphql", request.url?.absoluteString) + XCTAssertEqual("POST", request.httpMethod) + XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) + XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) + + let requestDict = try? JSONSerialization.jsonObject(with: request.httpBody!) as? [String: Any] + XCTAssert(OTUtils.compareDictionaries(expectedBody, requestDict!)) + } + + // MARK: - Success + + func testFetchQualifiedSegments_success() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(statusCode: 200, responseData: MockOdpUrlSession.goodResponseData)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: ["a", "b", "c"]) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, ["a"]) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_successWithEmptySegments() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(statusCode: 200, responseData: MockOdpUrlSession.goodEmptyResponseData)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: ["a", "b", "c"]) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, []) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + // MARK: - Failure + + func testFetchQualifiedSegments_invalidIdentifier() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(statusCode: 200, responseData: MockOdpUrlSession.invalidIdentifierResponseData)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + if case .invalidSegmentIdentifier = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_otherException() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(statusCode: 200, responseData: MockOdpUrlSession.otherExceptionResponseData)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + if case .fetchSegmentsFailed("TestExceptionClass") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_badResponse() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(statusCode: 200, responseData: MockOdpUrlSession.badResponseData)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + if case .fetchSegmentsFailed("decode error") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_networkError() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(withError: true)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + if case .fetchSegmentsFailed("network error") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_400() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(statusCode: 403, responseData: "Bad Request")) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + if case .fetchSegmentsFailed("403") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_500() { + let api = MockOdpSegmentApiManager(MockOdpUrlSession(statusCode: 500, responseData: "Server Error")) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + if case .fetchSegmentsFailed("500") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + // MARK: - Others + + func testMakeQuery() { + let api = OdpSegmentApiManager() + + let inputsForSegmentsToCheck = [ + [], + ["a", "b"] + ] + + let template = "query($userId: String, $audiences: [String]) {customer(key-1: $userId) {audiences(subset: $audiences) {edges {node {name state}}}}}" + let expectedBody = [ + [ + "query": template, + "variables":[ "audiences": [], "userId":"value-1"] + ], + [ + "query": template, + "variables":[ "audiences": ["a", "b"], "userId":"value-1"] + ] + ] + + for i in inputsForSegmentsToCheck.indices { + let query = api.makeQuery(userKey: "key-1", userValue: "value-1", segmentsToCheck: inputsForSegmentsToCheck[i]) + XCTAssert(OTUtils.compareDictionaries(expectedBody[i], query)) + } + } + + func testExtractComponent() { + let dict = ["a": ["b": ["c": "v"]]] + XCTAssertEqual(["b": ["c": "v"]], dict.extractComponent(keyPath: "a")) + XCTAssertEqual(["c": "v"], dict.extractComponent(keyPath: "a.b")) + XCTAssertEqual("v", dict.extractComponent(keyPath: "a.b.c")) + XCTAssertNil(dict.extractComponent(keyPath: "a.b.c.d")) + XCTAssertNil(dict.extractComponent(keyPath: "d")) + } + +} + +// MARK: - Tests with live ODP server +// tests below will be skipped in CI (travis/actions) since they use the live ODP server. +#if DEBUG + +extension OdpSegmentApiManagerTests { + + var odpApiKey: String { return "W4WzcEs-ABgXorzY7h1LCQ" } + var odpApiHost: String { return "https://api.zaius.com" } + var odpValidUserId: String { return "tester-101"} + + func testLiveOdpGraphQL() { + let manager = OdpSegmentApiManager() + + let sem = DispatchSemaphore(value: 0) + manager.fetchSegments(apiKey: odpApiKey, + apiHost: odpApiHost, + userKey: "fs_user_id", + userValue: odpValidUserId, + segmentsToCheck: ["segment-1"]) { segments, error in + XCTAssertNil(error) + XCTAssertEqual([], segments, "none of the test segments in the live ODP server") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + func testLiveOdpGraphQL_defaultParameters_userNotRegistered() { + let manager = OdpSegmentApiManager() + + let sem = DispatchSemaphore(value: 0) + manager.fetchSegments(apiKey: odpApiKey, + apiHost: odpApiHost, + userKey: "fs_user_id", + userValue: "not-registered-user-1", + segmentsToCheck: ["segment-1"]) { segments, error in + if case .invalidSegmentIdentifier = error { + XCTAssert(true) + + // [TODO] ODP server will fix to add this "InvalidSegmentIdentifier" later. + // Until then, use the old error format ("DataFetchingException"). + + } else if case .fetchSegmentsFailed("DataFetchingException") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } +} + +#endif + +// MARK: - MockOdpSegmentApiManager + +extension OdpSegmentApiManagerTests { + + class MockOdpSegmentApiManager: OdpSegmentApiManager { + let mockUrlSession: URLSession + + init(_ urlSession: URLSession) { + mockUrlSession = urlSession + } + + override func getSession() -> URLSession { + return mockUrlSession + } + } + + // MARK: - MockOdpUrlSession + + class MockOdpUrlSession: URLSession { + static var validSessions = 0 + var statusCode: Int + var withError: Bool + var responseData: String? + + class MockDataTask: URLSessionDataTask { + var task: () -> Void + + init(_ task: @escaping () -> Void) { + self.task = task + } + + override func resume() { + task() + } + } + + init(statusCode: Int = 0, withError: Bool = false, responseData: String? = nil) { + Self.validSessions += 1 + self.statusCode = statusCode + self.withError = withError + self.responseData = responseData ?? MockOdpUrlSession.goodResponseData + } + + override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + OdpSegmentApiManagerTests.createdApiRequest = request + + return MockDataTask() { + let statusCode = self.statusCode != 0 ? self.statusCode : 200 + let response = HTTPURLResponse(url: request.url!, + statusCode: statusCode, + httpVersion: nil, + headerFields: [String: String]()) + + let data = self.responseData?.data(using: .utf8) + let error = self.withError ? OptimizelyError.generic : nil + + completionHandler(data, response, error) + } + } + + override func finishTasksAndInvalidate() { + Self.validSessions -= 1 + } + + // MARK: - Utils + + static let goodResponseData: String = """ + { + "data": { + "customer": { + "audiences": { + "edges": [ + { + "node": { + "name": "a", + "state": "qualified", + "description": "qualifed sample" + } + }, + { + "node": { + "name": "b", + "state": "not_qualified", + "description": "not-qualified sample" + } + } + ] + } + } + } + } + """ + + static let goodEmptyResponseData: String = """ + { + "data": { + "customer": { + "audiences": { + "edges": [] + } + } + } + } + """ + + static let invalidIdentifierResponseData: String = """ + { + "errors": [ + { + "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "customer" + ], + "extensions": { + "classification": "InvalidIdentifierException" + } + } + ], + "data": { + "customer": null + } + } + """ + + static let otherExceptionResponseData: String = """ + { + "errors": [ + { + "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd", + "extensions": { + "classification": "TestExceptionClass" + } + } + ], + "data": { + "customer": null + } + } + """ + + static let badResponseData: String = """ + { + "data": {} + } + """ + } +} diff --git a/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift new file mode 100644 index 00000000..97a0133e --- /dev/null +++ b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift @@ -0,0 +1,173 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OdpSegmentManagerTests: XCTestCase { + var manager: OdpSegmentManager! + var odpConfig: OdpConfig! + var apiManager = MockOdpSegmentApiManager() + + var options = [OptimizelySegmentOption]() + + var userKey = "vuid" + var userValue = "test-user" + + override func setUp() { + odpConfig = OdpConfig() + + manager = OdpSegmentManager(cacheSize: 10, + cacheTimeoutInSecs: 10, + odpConfig: odpConfig, + apiManager: apiManager) + } + + func testFetchSegmentsSuccess_cacheMiss() { + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["new-customer"]) + + setCache(userKey, "123", ["a"]) + + let sem = DispatchSemaphore(value: 0) + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + options: options) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(["new-customer"], segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + + XCTAssertEqual("valid", apiManager.receivedApiKey) + XCTAssertEqual("host", apiManager.receivedApiHost) + } + + func testFetchSegmentsSuccess_cacheHit() { + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["new-customer"]) + + setCache(userKey, userValue, ["a"]) + + let sem = DispatchSemaphore(value: 0) + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + options: options) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(["a"], segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchSegmentsError() { + _ = odpConfig.update(apiKey: "invalid-key", apiHost: "host", segmentsToCheck: ["new-customer"]) + + let sem = DispatchSemaphore(value: 0) + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + options: []) { segments, error in + XCTAssertNotNil(error) + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + // MARK: - OptimizelySegmentOption + + func testOptions_ignoreCache() { + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["new-customer"]) + + setCache(userKey, userValue, ["a"]) + options = [.ignoreCache] + + let sem = DispatchSemaphore(value: 0) + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + options: options) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(["new-customer"], segments, "cache lookup should be skipped") + XCTAssertEqual(1, self.cacheCount, "cache save should be skipped as well") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testOptions_resetCache() { + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["new-customer"]) + + setCache(userKey, userValue, ["a"]) + setCache(userKey, "123", ["a"]) + setCache(userKey, "456", ["a"]) + options = [.resetCache] + + let sem = DispatchSemaphore(value: 0) + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + options: options) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(["new-customer"], segments, "cache lookup should be skipped") + XCTAssertEqual(segments, self.peekCache(self.userKey, self.userValue)) + XCTAssertEqual(1, self.cacheCount, "cache should be reset and then add a new one") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testMakeCacheKey() { + XCTAssertEqual("vuid-$-test-user", manager.makeCacheKey(userKey, userValue)) + } + + // MARK: - Utils + + func setCache(_ userKey: String, _ userValue: String, _ value: [String]) { + let cacheKey = manager.makeCacheKey(userKey, userValue) + manager.segmentsCache.save(key: cacheKey, value: value) + } + + func peekCache(_ userKey: String, _ userValue: String) -> [String]? { + let cacheKey = manager.makeCacheKey(userKey, userValue) + return manager.segmentsCache.peek(key: cacheKey) + } + + var cacheCount: Int { + return manager.segmentsCache.map.count + } + + // MARK: - MockOdpSegmentApiManager + + class MockOdpSegmentApiManager: OdpSegmentApiManager { + var receivedApiKey: String! + var receivedApiHost: String! + + override func fetchSegments(apiKey: String, + apiHost: String, + userKey: String, + userValue: String, + segmentsToCheck: [String], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + receivedApiKey = apiKey + receivedApiHost = apiHost + + DispatchQueue.global().async { + if apiKey == "invalid-key" { + completionHandler(nil, OptimizelyError.fetchSegmentsFailed("403")) + } else { + completionHandler(segmentsToCheck, nil) + } + } + } + } + +} diff --git a/Tests/OptimizelyTests-Common/OdpVuidManagerTests.swift b/Tests/OptimizelyTests-Common/OdpVuidManagerTests.swift new file mode 100644 index 00000000..ddf52e84 --- /dev/null +++ b/Tests/OptimizelyTests-Common/OdpVuidManagerTests.swift @@ -0,0 +1,55 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OdpVuidManagerTests: XCTestCase { + var manager = OdpVuidManager() + + func testMakeVuid() { + let vuid = manager.makeVuid() + + XCTAssertTrue(vuid.starts(with: "vuid_")) + XCTAssertEqual(vuid.count, 32) + } + + func testIsVuid() { + XCTAssertTrue(manager.isVuid(visitorId: "vuid_123")) + XCTAssertFalse(manager.isVuid(visitorId: "vuid-123")) + XCTAssertFalse(manager.isVuid(visitorId: "123")) + } + + func testAutoSaveAndLoad() { + UserDefaults.standard.removeObject(forKey: "optimizely-vuid") + + manager = OdpVuidManager() + let vuid1 = manager.vuid + + manager = OdpVuidManager() + let vuid2 = manager.vuid + + XCTAssertTrue(vuid1 == vuid2) + XCTAssert(manager.isVuid(visitorId: vuid1)) + XCTAssert(manager.isVuid(visitorId: vuid2)) + + UserDefaults.standard.removeObject(forKey: "optimizely-vuid") + + manager = OdpVuidManager() + let vuid3 = manager.vuid + + XCTAssertTrue(vuid1 != vuid3) + } +} diff --git a/Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift b/Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift deleted file mode 100644 index c49d452f..00000000 --- a/Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// Copyright 2019, 2021, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -import Optimizely -class OptimizelySDKTests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testOptimizelyClient1() { - // This is an example of a functional test case. - let json = "{\"version\": \"4\", \"rollouts\": [{\"experiments\": [{\"status\": \"Running\", \"key\": \"11214290043\", \"layerId\": \"11216280045\", \"trafficAllocation\": [{\"entityId\": \"11196890101\", \"endOfRange\": 0}], \"audienceIds\": [], \"variations\": [{\"variables\": [{\"id\": \"11196660143\", \"value\": \"\"}], \"id\": \"11196890101\", \"key\": \"11196890101\", \"featureEnabled\": true}], \"forcedVariations\": {}, \"id\": \"11214290043\"}], \"id\": \"11216280045\"}], \"typedAudiences\": [], \"anonymizeIP\": true, \"projectId\": \"11102097459\", \"variables\": [], \"featureFlags\": [{\"experimentIds\": [\"11174010269\"], \"rolloutId\": \"11216280045\", \"variables\": [{\"defaultValue\": \"\", \"type\": \"string\", \"id\": \"11196660143\", \"key\": \"string_variable\"}], \"id\": \"11216320075\", \"key\": \"my_feature\"}], \"experiments\": [{\"status\": \"Running\", \"key\": \"my_experiment\", \"layerId\": \"11186120103\", \"trafficAllocation\": [{\"entityId\": \"11193600046\", \"endOfRange\": 5000}, {\"entityId\": \"11198460034\", \"endOfRange\": 10000}], \"audienceIds\": [], \"variations\": [{\"variables\": [{\"id\": \"11196660143\", \"value\": \"\"}], \"id\": \"11193600046\", \"key\": \"variation_1\", \"featureEnabled\": true}, {\"variables\": [], \"id\": \"11198460034\", \"key\": \"variation_2\", \"featureEnabled\": false}], \"forcedVariations\": {}, \"id\": \"11174010269\"}, {\"status\": \"Running\", \"key\": \"background_experiment\", \"layerId\": \"11150133482\", \"trafficAllocation\": [{\"entityId\": \"11146534908\", \"endOfRange\": 5000}, {\"entityId\": \"11192561814\", \"endOfRange\": 10000}], \"audienceIds\": [], \"variations\": [{\"variables\": [], \"id\": \"11146534908\", \"key\": \"variation_a\"}, {\"variables\": [], \"id\": \"11192561814\", \"key\": \"variation_b\"}], \"forcedVariations\": {}, \"id\": \"11178792174\"}], \"audiences\": [], \"groups\": [], \"attributes\": [], \"botFiltering\": false, \"accountId\": \"8362480420\", \"events\": [{\"experimentIds\": [\"11174010269\", \"11178792174\"], \"id\": \"11173400866\", \"key\": \"sample_conversion\"}, {\"experimentIds\": [\"11174010269\"], \"id\": \"11196870086\", \"key\": \"my_conversion\"}, {\"experimentIds\": [], \"id\": \"12115533234\", \"key\": \"newevent\"}], \"revision\": \"10\"}" - let config = try! ProjectConfig(datafile: json) - - XCTAssertNotNil(config) - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testProjectConfigInvalidDatafile() { - var gotError = false - do { - _ = try ProjectConfig(datafile: "{revision:1}") - } catch { - gotError = true - } - XCTAssertTrue(gotError) - } - - func testOptimizelyClient2() { - let json = "{\"version\": \"4\", \"rollouts\": [{\"experiments\": [{\"status\": \"Running\", \"key\": \"11214290043\", \"layerId\": \"11216280045\", \"trafficAllocation\": [{\"entityId\": \"11196890101\", \"endOfRange\": 0}], \"audienceIds\": [], \"variations\": [{\"variables\": [{\"id\": \"11196660143\", \"value\": \"\"}], \"id\": \"11196890101\", \"key\": \"11196890101\", \"featureEnabled\": true}], \"forcedVariations\": {}, \"id\": \"11214290043\"}], \"id\": \"11216280045\"}], \"typedAudiences\": [], \"anonymizeIP\": true, \"projectId\": \"11102097459\", \"variables\": [], \"featureFlags\": [{\"experimentIds\": [\"11174010269\"], \"rolloutId\": \"11216280045\", \"variables\": [{\"defaultValue\": \"\", \"type\": \"string\", \"id\": \"11196660143\", \"key\": \"string_variable\"}], \"id\": \"11216320075\", \"key\": \"my_feature\"}], \"experiments\": [{\"status\": \"Running\", \"key\": \"my_experiment\", \"layerId\": \"11186120103\", \"trafficAllocation\": [{\"entityId\": \"11193600046\", \"endOfRange\": 5000}, {\"entityId\": \"11198460034\", \"endOfRange\": 10000}], \"audienceIds\": [], \"variations\": [{\"variables\": [{\"id\": \"11196660143\", \"value\": \"\"}], \"id\": \"11193600046\", \"key\": \"variation_1\", \"featureEnabled\": true}, {\"variables\": [], \"id\": \"11198460034\", \"key\": \"variation_2\", \"featureEnabled\": false}], \"forcedVariations\": {}, \"id\": \"11174010269\"}, {\"status\": \"Running\", \"key\": \"background_experiment\", \"layerId\": \"11150133482\", \"trafficAllocation\": [{\"entityId\": \"11146534908\", \"endOfRange\": 5000}, {\"entityId\": \"11192561814\", \"endOfRange\": 10000}], \"audienceIds\": [\"12097998496\"], \"variations\": [{\"variables\": [], \"id\": \"11146534908\", \"key\": \"variation_a\"}, {\"variables\": [], \"id\": \"11192561814\", \"key\": \"variation_b\"}], \"forcedVariations\": {}, \"id\": \"11178792174\"}], \"audiences\": [{\"id\": \"12097998496\", \"conditions\": \"[\\\"and\\\", [\\\"or\\\", [\\\"not\\\", [\\\"or\\\", {\\\"name\\\": \\\"testAttr\\\", \\\"type\\\": \\\"custom_attribute\\\", \\\"value\\\": \\\"some\\\"}]]]]\", \"name\": \"testAudience\"}], \"groups\": [], \"attributes\": [{\"id\": \"12248392446\", \"key\": \"testAttr\"}], \"botFiltering\": false, \"accountId\": \"8362480420\", \"events\": [{\"experimentIds\": [\"11174010269\", \"11178792174\"], \"id\": \"11173400866\", \"key\": \"sample_conversion\"}, {\"experimentIds\": [\"11174010269\"], \"id\": \"11196870086\", \"key\": \"my_conversion\"}, {\"experimentIds\": [], \"id\": \"12115533234\", \"key\": \"newevent\"}], \"revision\": \"12\"}" - - let config = try! ProjectConfig(datafile: json) - - XCTAssertNotNil(config) - - } - - func testOptimizelyClient3() { - let json = "{\"accountId\":\"2360254204\",\"anonymizeIP\":true,\"botFiltering\":true,\"projectId\":\"3918735994\",\"revision\":\"1480511547\",\"version\":\"4\",\"audiences\":[{\"id\":\"3468206642\",\"name\":\"Gryffindors\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"value\":\"Gryffindor\"}]]]},{\"id\":\"3988293898\",\"name\":\"Slytherins\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"value\":\"Slytherin\"}]]]},{\"id\":\"4194404272\",\"name\":\"english_citizens\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"}]]]},{\"id\":\"2196265320\",\"name\":\"audience_with_missing_value\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"},{\"name\":\"nationality\",\"type\":\"custom_attribute\"}]]]}],\"typedAudiences\":[{\"id\":\"3468206643\",\"name\":\"BOOL\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"booleanKey\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":true}]]]},{\"id\":\"3468206646\",\"name\":\"INTEXACT\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"integerKey\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":1}]]]},{\"id\":\"3468206644\",\"name\":\"INT\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"integerKey\",\"type\":\"custom_attribute\",\"match\":\"gt\",\"value\":1}]]]},{\"id\":\"3468206645\",\"name\":\"DOUBLE\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"doubleKey\",\"type\":\"custom_attribute\",\"match\":\"lt\",\"value\":100}]]]},{\"id\":\"3468206642\",\"name\":\"Gryffindors\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":\"Gryffindor\"}]]]},{\"id\":\"3988293898\",\"name\":\"Slytherins\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"match\":\"substring\",\"value\":\"Slytherin\"}]]]},{\"id\":\"4194404272\",\"name\":\"english_citizens\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":\"English\"}]]]},{\"id\":\"2196265320\",\"name\":\"audience_with_missing_value\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"},{\"name\":\"nationality\",\"type\":\"custom_attribute\"}]]]}],\"attributes\":[{\"id\":\"553339214\",\"key\":\"house\"},{\"id\":\"58339410\",\"key\":\"nationality\"},{\"id\":\"583394100\",\"key\":\"$opt_test\"},{\"id\":\"323434545\",\"key\":\"booleanKey\"},{\"id\":\"616727838\",\"key\":\"integerKey\"},{\"id\":\"808797686\",\"key\":\"doubleKey\"},{\"id\":\"808797686\",\"key\":\"\"}],\"events\":[{\"id\":\"3785620495\",\"key\":\"basic_event\",\"experimentIds\":[\"1323241596\",\"2738374745\",\"3042640549\",\"3262035800\",\"3072915611\"]},{\"id\":\"3195631717\",\"key\":\"event_with_paused_experiment\",\"experimentIds\":[\"2667098701\"]},{\"id\":\"1987018666\",\"key\":\"event_with_launched_experiments_only\",\"experimentIds\":[\"3072915611\"]}],\"experiments\":[{\"id\":\"1323241596\",\"key\":\"basic_experiment\",\"layerId\":\"1630555626\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767502\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458314\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767502\",\"endOfRange\":5000},{\"entityId\":\"3433458314\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{\"Harry Potter\":\"A\",\"Tom Riddle\":\"B\"}},{\"id\":\"1323241597\",\"key\":\"typed_audience_experiment\",\"layerId\":\"1630555627\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767503\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458315\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767503\",\"endOfRange\":5000},{\"entityId\":\"3433458315\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206643\",\"3468206644\",\"3468206646\",\"3468206645\"],\"audienceConditions\":[\"or\",\"3468206643\",\"3468206644\",\"3468206646\",\"3468206645\"],\"forcedVariations\":{}},{\"id\":\"1323241598\",\"key\":\"typed_audience_experiment_with_and\",\"layerId\":\"1630555628\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767504\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458316\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767504\",\"endOfRange\":5000},{\"entityId\":\"3433458316\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206643\",\"3468206644\",\"3468206645\"],\"audienceConditions\":[\"and\",\"3468206643\",\"3468206644\",\"3468206645\"],\"forcedVariations\":{}},{\"id\":\"1323241599\",\"key\":\"typed_audience_experiment_leaf_condition\",\"layerId\":\"1630555629\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767505\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458317\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767505\",\"endOfRange\":5000},{\"entityId\":\"3433458317\",\"endOfRange\":10000}],\"audienceIds\":[],\"audienceConditions\":\"3468206643\",\"forcedVariations\":{}},{\"id\":\"3262035800\",\"key\":\"multivariate_experiment\",\"layerId\":\"3262035800\",\"status\":\"Running\",\"variations\":[{\"id\":\"1880281238\",\"key\":\"Fred\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"F\"},{\"id\":\"4052219963\",\"value\":\"red\"}]},{\"id\":\"3631049532\",\"key\":\"Feorge\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"F\"},{\"id\":\"4052219963\",\"value\":\"eorge\"}]},{\"id\":\"4204375027\",\"key\":\"Gred\",\"featureEnabled\":false,\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"red\"}]},{\"id\":\"2099211198\",\"key\":\"George\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"eorge\"}]}],\"trafficAllocation\":[{\"entityId\":\"1880281238\",\"endOfRange\":2500},{\"entityId\":\"3631049532\",\"endOfRange\":5000},{\"entityId\":\"4204375027\",\"endOfRange\":7500},{\"entityId\":\"2099211198\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Fred\":\"Fred\",\"Feorge\":\"Feorge\",\"Gred\":\"Gred\",\"George\":\"George\"}},{\"id\":\"2201520193\",\"key\":\"double_single_variable_feature_experiment\",\"layerId\":\"1278722008\",\"status\":\"Running\",\"variations\":[{\"id\":\"1505457580\",\"key\":\"pi_variation\",\"featureEnabled\":true,\"variables\":[{\"id\":\"4111654444\",\"value\":\"3.14\"}]},{\"id\":\"119616179\",\"key\":\"euler_variation\",\"variables\":[{\"id\":\"4111654444\",\"value\":\"2.718\"}]}],\"trafficAllocation\":[{\"entityId\":\"1505457580\",\"endOfRange\":4000},{\"entityId\":\"119616179\",\"endOfRange\":8000}],\"audienceIds\":[\"3988293898\"],\"forcedVariations\":{}},{\"id\":\"2667098701\",\"key\":\"paused_experiment\",\"layerId\":\"3949273892\",\"status\":\"Paused\",\"variations\":[{\"id\":\"391535909\",\"key\":\"Control\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"391535909\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{\"Harry Potter\":\"Control\"}},{\"id\":\"3072915611\",\"key\":\"launched_experiment\",\"layerId\":\"3587821424\",\"status\":\"Launched\",\"variations\":[{\"id\":\"1647582435\",\"key\":\"launch_control\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1647582435\",\"endOfRange\":8000}],\"audienceIds\":[],\"forcedVariations\":{}},{\"id\":\"748215081\",\"key\":\"experiment_with_malformed_audience\",\"layerId\":\"1238149537\",\"status\":\"Running\",\"variations\":[{\"id\":\"535538389\",\"key\":\"var1\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"535538389\",\"endOfRange\":10000}],\"audienceIds\":[\"2196265320\"],\"forcedVariations\":{}}],\"groups\":[{\"id\":\"1015968292\",\"policy\":\"random\",\"experiments\":[{\"id\":\"2738374745\",\"key\":\"first_grouped_experiment\",\"layerId\":\"3301900159\",\"status\":\"Running\",\"variations\":[{\"id\":\"2377378132\",\"key\":\"A\",\"variables\":[]},{\"id\":\"1179171250\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"2377378132\",\"endOfRange\":5000},{\"entityId\":\"1179171250\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Harry Potter\":\"A\",\"Tom Riddle\":\"B\"}},{\"id\":\"3042640549\",\"key\":\"second_grouped_experiment\",\"layerId\":\"2625300442\",\"status\":\"Running\",\"variations\":[{\"id\":\"1558539439\",\"key\":\"A\",\"variables\":[]},{\"id\":\"2142748370\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1558539439\",\"endOfRange\":5000},{\"entityId\":\"2142748370\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Hermione Granger\":\"A\",\"Ronald Weasley\":\"B\"}}],\"trafficAllocation\":[{\"entityId\":\"2738374745\",\"endOfRange\":4000},{\"entityId\":\"3042640549\",\"endOfRange\":8000}]},{\"id\":\"2606208781\",\"policy\":\"random\",\"experiments\":[{\"id\":\"4138322202\",\"key\":\"mutex_group_2_experiment_1\",\"layerId\":\"3755588495\",\"status\":\"Running\",\"variations\":[{\"id\":\"1394671166\",\"key\":\"mutex_group_2_experiment_1_variation_1\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2059187672\",\"value\":\"mutex_group_2_experiment_1_variation_1\"}]}],\"audienceIds\":[],\"forcedVariations\":{},\"trafficAllocation\":[{\"entityId\":\"1394671166\",\"endOfRange\":10000}]},{\"id\":\"1786133852\",\"key\":\"mutex_group_2_experiment_2\",\"layerId\":\"3818002538\",\"status\":\"Running\",\"variations\":[{\"id\":\"1619235542\",\"key\":\"mutex_group_2_experiment_2_variation_2\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2059187672\",\"value\":\"mutex_group_2_experiment_2_variation_2\"}]}],\"trafficAllocation\":[{\"entityId\":\"1619235542\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{}}],\"trafficAllocation\":[{\"entityId\":\"4138322202\",\"endOfRange\":5000},{\"entityId\":\"1786133852\",\"endOfRange\":10000}]}],\"featureFlags\":[{\"id\":\"4195505407\",\"key\":\"boolean_feature\",\"rolloutId\":\"\",\"experimentIds\":[],\"variables\":[]},{\"id\":\"3926744821\",\"key\":\"double_single_variable_feature\",\"rolloutId\":\"\",\"experimentIds\":[\"2201520193\"],\"variables\":[{\"id\":\"4111654444\",\"key\":\"double_variable\",\"type\":\"double\",\"defaultValue\":\"14.99\"}]},{\"id\":\"3281420120\",\"key\":\"integer_single_variable_feature\",\"rolloutId\":\"2048875663\",\"experimentIds\":[],\"variables\":[{\"id\":\"593964691\",\"key\":\"integer_variable\",\"type\":\"integer\",\"defaultValue\":\"7\"}]},{\"id\":\"2591051011\",\"key\":\"boolean_single_variable_feature\",\"rolloutId\":\"\",\"experimentIds\":[],\"variables\":[{\"id\":\"3974680341\",\"key\":\"boolean_variable\",\"type\":\"boolean\",\"defaultValue\":\"true\"}]},{\"id\":\"2079378557\",\"key\":\"string_single_variable_feature\",\"rolloutId\":\"1058508303\",\"experimentIds\":[],\"variables\":[{\"id\":\"2077511132\",\"key\":\"string_variable\",\"type\":\"string\",\"defaultValue\":\"wingardium leviosa\"}]},{\"id\":\"3263342226\",\"key\":\"multi_variate_feature\",\"rolloutId\":\"813411034\",\"experimentIds\":[\"3262035800\"],\"variables\":[{\"id\":\"675244127\",\"key\":\"first_letter\",\"type\":\"string\",\"defaultValue\":\"H\"},{\"id\":\"4052219963\",\"key\":\"rest_of_name\",\"type\":\"string\",\"defaultValue\":\"arry\"}]},{\"id\":\"3263342226\",\"key\":\"mutex_group_feature\",\"rolloutId\":\"\",\"experimentIds\":[\"4138322202\",\"1786133852\"],\"variables\":[{\"id\":\"2059187672\",\"key\":\"correlating_variation_name\",\"type\":\"string\",\"defaultValue\":null}]}],\"rollouts\":[{\"id\":\"1058508303\",\"experiments\":[{\"id\":\"1785077004\",\"key\":\"1785077004\",\"status\":\"Running\",\"layerId\":\"1058508303\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"1566407342\",\"key\":\"1566407342\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2077511132\",\"value\":\"lumos\"}]}],\"trafficAllocation\":[{\"entityId\":\"1566407342\",\"endOfRange\":5000}]}]},{\"id\":\"813411034\",\"experiments\":[{\"id\":\"3421010877\",\"key\":\"3421010877\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"521740985\",\"key\":\"521740985\",\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"odric\"}]}],\"trafficAllocation\":[{\"entityId\":\"521740985\",\"endOfRange\":5000}]},{\"id\":\"600050626\",\"key\":\"600050626\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"3988293898\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"180042646\",\"key\":\"180042646\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"S\"},{\"id\":\"4052219963\",\"value\":\"alazar\"}]}],\"trafficAllocation\":[{\"entityId\":\"180042646\",\"endOfRange\":5000}]},{\"id\":\"2637642575\",\"key\":\"2637642575\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"4194404272\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"2346257680\",\"key\":\"2346257680\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"D\"},{\"id\":\"4052219963\",\"value\":\"udley\"}]}],\"trafficAllocation\":[{\"entityId\":\"2346257680\",\"endOfRange\":5000}]},{\"id\":\"828245624\",\"key\":\"828245624\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"3137445031\",\"key\":\"3137445031\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"M\"},{\"id\":\"4052219963\",\"value\":\"uggle\"}]}],\"trafficAllocation\":[{\"entityId\":\"3137445031\",\"endOfRange\":5000}]}]},{\"id\":\"2048875663\",\"experiments\":[{\"id\":\"3794675122\",\"key\":\"3794675122\",\"status\":\"Running\",\"layerId\":\"2048875663\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"589640735\",\"key\":\"589640735\",\"featureEnabled\":true,\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"589640735\",\"endOfRange\":10000}]}]}],\"variables\":[]}" - - let config = try! ProjectConfig(datafile: json) - - for audience in config.project.typedAudiences! { - var attr = ["integerKey": 1, "doubleKey": 99.0, "booleanKey": true, "nationality": "English"] as [String: Any] - // all user attributes equate to true at this point. so, all conditions should pass. - if audience.name == "INT" { - attr["integerKey"] = 2 - } - if ["Gryffindors", "Slytherins"].contains(where: { $0 == audience.name }) { - if "Gryffindors" == audience.name { - attr["house"] = "Gryffindor" - } else { - attr["house"] = "Slytherin" - } - } - - XCTAssertTrue(try! audience.conditionHolder.evaluate(project: config.project, attributes: attr)) - } - XCTAssertNotNil(config) - - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift new file mode 100644 index 00000000..cc3c2317 --- /dev/null +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -0,0 +1,281 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OptimizelyUserContextTests_ODP: XCTestCase { + + var optimizely: OptimizelyClient! + var user: OptimizelyUserContext! + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + var odpManager: MockOdpManager! + + let kUserId = "tester" + let kUserKey = "custom_id" + let kUserValue = "custom_id_value" + let sdkKey = OTUtils.randomSdkKey + + override func setUp() { + odpManager = MockOdpManager(sdkKey: sdkKey, disable: false, cacheSize: 10, cacheTimeoutInSecs: 10) + + optimizely = OptimizelyClient(sdkKey: sdkKey) + optimizely.odpManager = odpManager + + user = optimizely.createUserContext(userId: kUserId) + } + + // MARK: - identify + + func testIdentifyCalledAutomatically() { + sleep(1) + XCTAssertEqual(true, odpManager.identifyCalled, "identifyUser is implicitly called on UserContext init") + XCTAssertEqual(kUserId, odpManager.userId) + } + + // MARK: - isQualifiedFor + + func testIsQualifiedFor() { + XCTAssertFalse(user.isQualifiedFor(segment: "a")) + + user.qualifiedSegments = ["a", "b"] + XCTAssertTrue(user.isQualifiedFor(segment: "a")) + XCTAssertFalse(user.isQualifiedFor(segment: "x")) + + user.qualifiedSegments = [] + XCTAssertFalse(user.isQualifiedFor(segment: "a")) + } + + // MARK: - Success + + func testFetchQualifiedSegments_successDefaultUser() { + try? optimizely.start(datafile: datafile) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments { segments, error in + XCTAssertNil(error) + XCTAssertEqual(["odp-segment-1"], segments) + XCTAssertEqual(self.user.qualifiedSegments, segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + + // MARK: - Failure + + func testFetchQualifiedSegments_sdkNotReady() { + user.optimizely = nil + user.qualifiedSegments = ["dummy"] + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments { segments, error in + XCTAssertEqual(OptimizelyError.sdkNotReady.reason, error?.reason) + XCTAssertNil(segments) + XCTAssertNil(self.user.qualifiedSegments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + + func testFetchQualifiedSegments_fetchFailed() { + user.qualifiedSegments = ["dummy"] + + // ODP apiKey is not available + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments { segments, error in + XCTAssertNotNil(error) + XCTAssertNil(segments) + XCTAssertNil(self.user.qualifiedSegments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + + // MARK: - SegmentsToCheck + + func testFetchQualifiedSegments_segmentsToCheck_validAfterStart() { + try? optimizely.start(datafile: datafile) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments { _, _ in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(odpManager.odpConfig.segmentsToCheck)) + } + + func testFetchQualifiedSegments_segmentsNotUsed() { + let datafile = OTUtils.loadJSONDatafile("odp_integrated_no_segments")! + try? optimizely.start(datafile: datafile) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, []) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + +} + +// MARK: - Optional parameters + +extension OptimizelyUserContextTests_ODP { + + func testFetchQualifiedSegments_parameters() { + try? optimizely.start(datafile: datafile) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(options: [.ignoreCache]) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, ["odp-segment-1"]) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + + XCTAssertEqual(kUserId, odpManager.userId, "userId should be used as a default") + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(odpManager.odpConfig.segmentsToCheck), "segmentsToCheck should be all-in-project by default") + XCTAssertEqual([.ignoreCache], odpManager.options) + } + + func testFetchQualifiedSegments_configReady() { + XCTAssertNil(odpManager.odpConfig.apiKey) + XCTAssertNil(odpManager.odpConfig.apiHost) + + try? optimizely.start(datafile: "invalid") + + XCTAssertNil(odpManager.odpConfig.apiKey) + XCTAssertNil(odpManager.odpConfig.apiHost) + + try! optimizely.start(datafile: datafile) + + XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", odpManager.odpConfig.apiKey) + XCTAssertEqual("https://api.zaius.com", odpManager.odpConfig.apiHost) + + let datafile = OTUtils.loadJSONDatafile("decide_datafile")! // no integration in this datafile + try! optimizely.start(datafile: datafile) + + XCTAssertNil(odpManager.odpConfig.apiKey) + XCTAssertNil(odpManager.odpConfig.apiHost) + } + +} + +// MARK: - Tests with live ODP server + +// tests below will be skipped in CI (travis/actions) since they use the live ODP server. +#if DEBUG + +extension OptimizelyUserContextTests_ODP { + // {"vuid": "00TEST00VUID00FULLSTACK", "fs_user_id": "tester-101"} bound in ODP server for testing + var testOdpUserKey: String { return "fs_user_id" } + var testOdpUserId: String { return "tester-101"} + + func testLiveOdpGraphQL() { + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + try! optimizely.start(datafile: datafile) + let user = optimizely.createUserContext(userId: testOdpUserId) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments { segments, error in + XCTAssertNil(error) + XCTAssertEqual([], segments, "none of the test segments in the live ODP server") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + /* + this test is not good since createUserContext with not-registered-user will auto register the user. + the same live odp test with not-registered-user will be done in OdpSegmentApiManagerTests, so skipped here. + + func testLiveOdpGraphQL_defaultParameters_userNotRegistered() { + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + try! optimizely.start(datafile: datafile) + let user = optimizely.createUserContext(userId: "not-registered-user") + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments { segments, error in + if case .invalidSegmentIdentifier = error { + XCTAssert(true) + + // [TODO] ODP server will fix to add this "InvalidSegmentIdentifier" later. + // Until then, use the old error format ("DataFetchingException"). + + } else if case .fetchSegmentsFailed("DataFetchingException") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + */ +} + +#endif + +// MARK: - MockOdpManager + +class MockOdpManager: OdpManager { + var userId: String? + var options: [OptimizelySegmentOption]! + var identifyCalled = false + + init(sdkKey: String, disable: Bool, cacheSize: Int, cacheTimeoutInSecs: Int) { + super.init(sdkKey: sdkKey, disable: disable, cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeoutInSecs) + self.segmentManager?.apiMgr = MockOdpSegmentApiManager() + } + + override func fetchQualifiedSegments(userId: String, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + self.userId = userId + self.options = options + super.fetchQualifiedSegments(userId: userId, options: options, completionHandler: completionHandler) + } + + override func identifyUser(userId: String) { + self.userId = userId + self.identifyCalled = true + } +} + +// MARK: - MockOdpSegmentApiManager + +class MockOdpSegmentApiManager: OdpSegmentApiManager { + var receivedApiKey: String! + var receivedApiHost: String! + + override func fetchSegments(apiKey: String, + apiHost: String, + userKey: String, + userValue: String, + segmentsToCheck: [String], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + receivedApiKey = apiKey + receivedApiHost = apiHost + + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + let qualified = segmentsToCheck.isEmpty ? [] : [segmentsToCheck.sorted{ $0 < $1 }.first!] + completionHandler(qualified, nil) + } + } +} diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_2.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_2.swift new file mode 100644 index 00000000..9a71b8c4 --- /dev/null +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_2.swift @@ -0,0 +1,82 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OptimizelyUserContextTests_ODP_2: XCTestCase { + + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + + func testOdpEvents_earlyEventsDispatched() { + // use a different sdkKey to avoid events conflict + let sdkKey = OTUtils.randomSdkKey + + // odp disabled to avoid initial noise + + let optimizely = OptimizelyClient(sdkKey: sdkKey, + settings: OptimizelySdkSettings(disableOdp: true)) + + // override with a custom enabled odpManager. + // - client_inializatied event will be sent automatically + // - will wait in the queue until project config is ready + + let odpEventApiManager = MockOdpEventApiManager() + optimizely.odpManager = OdpManager(sdkKey: sdkKey, + disable: false, + cacheSize: 10, + cacheTimeoutInSecs: 10, + eventManager: OdpEventManager(sdkKey: sdkKey, + odpConfig: nil, + apiManager: odpEventApiManager)) + + // identified event will sent but wait in the queue until project config is ready + _ = optimizely.createUserContext(userId: "tester") + + sleep(1) + XCTAssertEqual(odpEventApiManager.dispatchedEvents.count, 0, "wait until project config is ready") + + // project config gets ready + try! optimizely.start(datafile: datafile) + + // identified event will sent + _ = optimizely.createUserContext(userId: "tester") + + sleep(1) + XCTAssertEqual(odpEventApiManager.dispatchedEvents.count, 3, "client_initialized and 2 x identified events") + + for i in 0..<100 { + _ = optimizely.createUserContext(userId: "tester-\(i % 10)") + } + + sleep(1) + XCTAssertEqual(odpEventApiManager.dispatchedEvents.count, 103, "100 more identified events") + } + + // MARK: - Utils + + class MockOdpEventApiManager: OdpEventApiManager { + var dispatchedEvents = [OdpEvent]() + + override func sendOdpEvents(apiKey: String, + apiHost: String, + events: [OdpEvent], + completionHandler: @escaping (OptimizelyError?) -> Void) { + self.dispatchedEvents.append(contentsOf: events) + completionHandler(nil) + } + } + +} diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_Decide.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_Decide.swift new file mode 100644 index 00000000..82b4e898 --- /dev/null +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_Decide.swift @@ -0,0 +1,86 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OptimizelyUserContextTests_ODP_Decide: XCTestCase { + + var optimizely: OptimizelyClient! + var user: OptimizelyUserContext! + let kUserId = "tester" + let kFlagKey = "flag-segment" + + override func setUp() { + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, defaultLogLevel: .info) + try! optimizely.start(datafile: datafile) + user = optimizely.createUserContext(userId: kUserId) + } + + func testDecideWithQualifiedSegments_segmentHitInABTest() { + user = optimizely.createUserContext(userId: kUserId) + user.qualifiedSegments = ["odp-segment-1", "odp-segment-none"] + + let decision = user.decide(key: kFlagKey, options: [.ignoreUserProfileService]) + + XCTAssertEqual(decision.variationKey, "variation-a") + } + + func testDecideWithQualifiedSegments_otherAudienceHitInABTest() { + user = optimizely.createUserContext(userId: kUserId, attributes: ["age": 30]) + user.qualifiedSegments = ["odp-segment-none"] + + let decision = user.decide(key: kFlagKey, options: [.ignoreUserProfileService]) + + XCTAssertEqual(decision.variationKey, "variation-a") + } + + func testDecideWithQualifiedSegments_segmentHitInRollout() { + user = optimizely.createUserContext(userId: kUserId) + user.qualifiedSegments = ["odp-segment-2"] + + let decision = user.decide(key: kFlagKey, options: [.ignoreUserProfileService]) + + XCTAssertEqual(decision.variationKey, "rollout-variation-on") + } + + func testDecideWithQualifiedSegments_segmentMissInRollout() { + user = optimizely.createUserContext(userId: kUserId) + user.qualifiedSegments = ["odp-segment-none"] + + let decision = user.decide(key: kFlagKey, options: [.ignoreUserProfileService]) + + XCTAssertEqual(decision.variationKey, "rollout-variation-off") + } + + func testDecideWithQualifiedSegments_emptySegments() { + user = optimizely.createUserContext(userId: kUserId) + user.qualifiedSegments = [] + + let decision = user.decide(key: kFlagKey, options: [.ignoreUserProfileService]) + + XCTAssertEqual(decision.variationKey, "rollout-variation-off") + } + + func testDecideWithQualifiedSegments_default() { + user = optimizely.createUserContext(userId: kUserId) + + let decision = user.decide(key: kFlagKey, options: [.ignoreUserProfileService]) + + XCTAssertEqual(decision.variationKey, "rollout-variation-off") + } + +} diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m index df4d840f..26fb7f36 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m @@ -39,16 +39,12 @@ - (void)setUp { // MARK: - UserContext - (void)testUserContext { - OptimizelyUserContext *user = [[OptimizelyUserContext alloc] initWithOptimizely:self.optimizely - userId:kUserId - attributes:nil]; + OptimizelyUserContext *user = [self.optimizely createUserContextWithUserId:kUserId attributes:nil]; XCTAssert([user.optimizely isEqual:self.optimizely]); XCTAssert([user.userId isEqualToString:kUserId]); XCTAssert(user.attributes.count == 0); - user = [[OptimizelyUserContext alloc] initWithOptimizely:self.optimizely - userId:kUserId - attributes:@{@"country": @"US", @"age": @"18"}]; + user = [self.optimizely createUserContextWithUserId:kUserId attributes:@{@"country": @"US", @"age": @"18"}]; XCTAssert([user.optimizely isEqual:self.optimizely]); XCTAssert([user.userId isEqualToString:kUserId]); XCTAssert([user.attributes[@"country"] isEqualToString:@"US"]); diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Performance.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Performance.swift new file mode 100644 index 00000000..3772b079 --- /dev/null +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Performance.swift @@ -0,0 +1,123 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class OptimizelyUserContextTests_Performance: XCTestCase { + var optimizely: OptimizelyClient! + + override func setUpWithError() throws { + let datafile = OTUtils.loadJSONDatafile("decide_datafile")! + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, defaultLogLevel: .error) + try! optimizely.start(datafile: datafile) + } +} + +// tests below will be skipped in CI (travis/actions) since they use time control. + +#if DEBUG + +extension OptimizelyUserContextTests_Performance { + + func testPerformance_create() { + let timeInMicrosecs = measureTime { + _ = optimizely.createUserContext(userId: "tester", attributes: ["a1": "b1"]) + } + + XCTAssert(timeInMicrosecs < 10, "user context create takes too long (\(timeInMicrosecs) microsecs)") + } + + func testPerformance_clone() { + let user = OptimizelyUserContext(optimizely: optimizely, userId: "tester", attributes: ["a1": "b1"]) + + let timeInMicrosecs = measureTime { + _ = user.clone + } + + XCTAssert(timeInMicrosecs < 10, "user context cloning takes too long (\(timeInMicrosecs) microsecs)") + } + + func testPerformance_clone_2() { + let user = OptimizelyUserContext(optimizely: optimizely, userId: "tester", attributes: ["a1": "b1"]) + + for i in 0..<100 { + user.setAttribute(key: "k\(i)", value: "v\(i)") + } + for i in 0..<100 { + _ = user.setForcedDecision(context: OptimizelyDecisionContext(flagKey: "f\(i)", ruleKey: "k\(i)"), decision: OptimizelyForcedDecision(variationKey: "v\(i)")) + } + user.qualifiedSegments = (0..<100).map{ "segment\($0)" } + + let timeInMicrosecs = measureTime { + _ = user.clone + } + + XCTAssert(timeInMicrosecs < 10, "user context cloning takes too long (\(timeInMicrosecs) microsecs)") + } + + func testPerformance_decideInvalid() { + let user = OptimizelyUserContext(optimizely: optimizely, userId: "tester", attributes: ["a1": "b1"]) + + var decision: OptimizelyDecision! + let timeInMicrosecs = measureTime { + decision = user.decide(key: "invalid") + } + XCTAssertFalse(decision.enabled) + + XCTAssert(timeInMicrosecs < 10, "user context decide-with-invalid takes too long (\(timeInMicrosecs) microsecs)") + } + + func testPerformance_decideValid() { + + // fall-thru to 'everyone-else after one ab-test (audience=gender) + 2 rollouts (audience=country, audience=browser) + let featureKey = "feature_1" + + let user = optimizely.createUserContext(userId: "tester") + + var decision: OptimizelyDecision! + let timeInMicrosecs = measureTime { + decision = user.decide(key: featureKey, options: [.ignoreUserProfileService]) + } + XCTAssert(decision.enabled, "everyone-else rule is enabled") + + XCTAssert(timeInMicrosecs < 300, "user context decide-with-valid takes too long (\(timeInMicrosecs) microsecs)") + } + +} + +#endif + +// MARK: - Utils + +extension OptimizelyUserContextTests_Performance { + + func measureTime(operation: () -> Void) -> Double { + let measureCount = 10000 + + let startTime = CFAbsoluteTimeGetCurrent() + for _ in 0.. Audience { + let data: [String: Any] = ["id": "12345", "name": "group-a", "conditions": conditions] + return try! OTUtils.model(from: data) + } + +} diff --git a/Tests/OptimizelyTests-DataModel/AudienceTests_Evaluate.swift b/Tests/OptimizelyTests-DataModel/AudienceTests_Evaluate.swift index 53223f33..d47a0538 100644 --- a/Tests/OptimizelyTests-DataModel/AudienceTests_Evaluate.swift +++ b/Tests/OptimizelyTests-DataModel/AudienceTests_Evaluate.swift @@ -84,24 +84,24 @@ class AudienceTests_Evaluate: XCTestCase { func testEvaluateConditionsMatch() { let audience = makeAudienceLegacy(conditions: kAudienceConditions) - - let attributesPassOrValue = ["device_type": "iPhone", - "location": "San Francisco", - "browser": "Chrome"] - XCTAssertTrue(try! audience.evaluate(project: nil, attributes: attributesPassOrValue)) + let attributes = ["device_type": "iPhone", + "location": "San Francisco", + "browser": "Chrome"] + + XCTAssertTrue(try! audience.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testEvaluateConditionsDoNotMatch() { let audience = makeAudienceLegacy(conditions: kAudienceConditions) - let attributesPassOrValue = ["device_type": "iPhone", - "location": "San Francisco", - "browser": "Firefox"] + let attributes = ["device_type": "iPhone", + "location": "San Francisco", + "browser": "Firefox"] - XCTAssertFalse(try! audience.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertFalse(try! audience.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } - + func testEvaluateSingleLeaf() { let config = self.optimizely.config @@ -109,7 +109,7 @@ class AudienceTests_Evaluate: XCTestCase { let attributes = ["house": "Gryffindor"] - let bool = try? holder.evaluate(project: config?.project, attributes: attributes) + let bool = try? holder.evaluate(project: config?.project, user: OTUtils.user(attributes: attributes)) XCTAssertTrue(bool!) } @@ -117,26 +117,26 @@ class AudienceTests_Evaluate: XCTestCase { func testEvaluateEmptyUserAttributes() { let audience = makeAudienceLegacy(conditions: kAudienceConditions) - let attributesPassOrValue = [String: String]() - XCTAssertNil(try? audience.evaluate(project: nil, attributes: attributesPassOrValue)) + let attributes = [String: String]() + XCTAssertNil(try? audience.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testEvaluateNullUserAttributes() { let audience = makeAudienceLegacy(conditions: kAudienceConditions) - XCTAssertNil(try? audience.evaluate(project: nil, attributes: nil)) + XCTAssertNil(try? audience.evaluate(project: nil, user: OTUtils.user(attributes: nil))) } func testTypedUserAttributesEvaluateTrue() { let audience = makeAudience(conditions: kAudienceConditionsWithAnd) - let attributesPassOrValue: [String: Any] = ["device_type": "iPhone", + let attributes: [String: Any] = ["device_type": "iPhone", "is_firefox": false, "num_users": 15, "pi_value": 3.14, "decimal_value": 3.15678] - XCTAssertTrue(try! audience.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertTrue(try! audience.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testEvaluateTrueWhenNoUserAttributesAndConditionEvaluatesTrue() { @@ -145,33 +145,33 @@ class AudienceTests_Evaluate: XCTestCase { let conditions: [Any] = ["not", ["or", ["or", ["name": "input_value", "type": "custom_attribute", "match": "exists"]]]] let audience = makeAudience(conditions: conditions) - XCTAssertTrue(try! audience.evaluate(project: nil, attributes: nil)) + XCTAssertTrue(try! audience.evaluate(project: nil, user: OTUtils.user(attributes: nil))) } // MARK: - Invalid Base Condition Tests func testEvaluateReturnsNullWithInvalidBaseCondition() { - let attributesPassOrValue = ["device_type": "iPhone"] + let attributes = ["device_type": "iPhone"] var condition = ["name": "device_type"] var userAttribute: UserAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) condition = ["name": "device_type", "value": "iPhone"] userAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) condition = ["name": "device_type", "match": "exact"] userAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) condition = ["name": "device_type", "type": "invalid"] userAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) condition = ["name": "device_type", "type": "custom_attribute"] userAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) } // MARK: - Invalid input Tests @@ -182,9 +182,9 @@ class AudienceTests_Evaluate: XCTestCase { "type": "invalid", "match": "exact"] - let attributesPassOrValue = ["device_type": "iPhone"] + let attributes = ["device_type": "iPhone"] let userAttribute: UserAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateReturnsNullWithNullValueTypeAndNonExistMatchType() { @@ -209,22 +209,22 @@ class AudienceTests_Evaluate: XCTestCase { "type": "custom_attribute", "match": "lt"] - let attributesPassOrValue = ["device_type": "iPhone"] + let attributes = ["device_type": "iPhone"] var userAttribute: UserAttribute = try! OTUtils.model(from: condition1) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) userAttribute = try! OTUtils.model(from: condition2) - XCTAssertTrue(try! userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertTrue(try! userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) userAttribute = try! OTUtils.model(from: condition3) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) userAttribute = try! OTUtils.model(from: condition4) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) userAttribute = try! OTUtils.model(from: condition5) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateReturnsNullWithInvalidMatchType() { @@ -233,10 +233,10 @@ class AudienceTests_Evaluate: XCTestCase { "type": "custom_attribute", "match": "invalid"] - let attributesPassOrValue = ["device_type": "iPhone"] + let attributes = ["device_type": "iPhone"] let userAttribute: UserAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateReturnsNullWithInvalidValueForMatchType() { @@ -245,10 +245,10 @@ class AudienceTests_Evaluate: XCTestCase { "type": "custom_attribute", "match": "substring"] - let attributesPassOrValue = ["is_firefox": false] + let attributes = ["is_firefox": false] let userAttribute: UserAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) } // MARK: - ExactMatcher Tests @@ -259,136 +259,136 @@ class AudienceTests_Evaluate: XCTestCase { "type": "custom_attribute", "match": "exact"] - let attributesPassOrValue = ["device_type": "iPhone"] + let attributes = ["device_type": "iPhone"] let userAttribute: UserAttribute = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(user: OTUtils.user(attributes: attributes))) } func testExactMatcherReturnsNullWhenNoUserProvidedValue() { - let attributesPassOrValue: [String: Any] = [:] + let attributes: [String: Any] = [:] var conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchStringType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchBoolType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchDecimalType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchIntType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testExactMatcherReturnsFalseWhenAttributeValueDoesNotMatch() { - let attributesPassOrValue1 = ["attr_value": "chrome"] - let attributesPassOrValue2 = ["attr_value": true] - let attributesPassOrValue3 = ["attr_value": 2.5] - let attributesPassOrValue4 = ["attr_value": 55] + let attributes1 = ["attr_value": "chrome"] + let attributes2 = ["attr_value": true] + let attributes3 = ["attr_value": 2.5] + let attributes4 = ["attr_value": 55] var conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchStringType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchBoolType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchDecimalType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue3)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes3))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchIntType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue4)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes4))) } func testExactMatcherReturnsNullWhenTypeMismatch() { - let attributesPassOrValue1 = ["attr_value": true] - let attributesPassOrValue2 = ["attr_value": "abcd"] - let attributesPassOrValue3 = ["attr_value": false] - let attributesPassOrValue4 = ["attr_value": "apple"] - let attributesPassOrValue5 = [String: String]() - //let attributesPassOrValue6 = ["attr_value" : nil] + let attributes1 = ["attr_value": true] + let attributes2 = ["attr_value": "abcd"] + let attributes3 = ["attr_value": false] + let attributes4 = ["attr_value": "apple"] + let attributes5 = [String: String]() + //let attributes6 = ["attr_value" : nil] var conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchStringType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchBoolType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchDecimalType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue3)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes3))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchIntType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue4)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue5)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes4))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes5))) } func testExactMatcherReturnsNullWithNumericInfinity() { // TODO: [Jae] confirm: do we need this inifinite case for Swift? Not parsed OK (invalid) - let attributesPassOrValue1 = ["attr_value": Double.infinity] + let attributes = ["attr_value": Double.infinity] let andCondition1: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchIntType) - XCTAssertNil(try? andCondition1.evaluate(project: nil, attributes: attributesPassOrValue1)) + XCTAssertNil(try? andCondition1.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testExactMatcherReturnsTrueWhenAttributeValueMatches() { - let attributesPassOrValue1 = ["attr_value": "firefox"] - let attributesPassOrValue2 = ["attr_value": false] - let attributesPassOrValue3 = ["attr_value": 1.5] - let attributesPassOrValue4 = ["attr_value": 10] - let attributesPassOrValue5 = ["attr_value": 10.0] + let attributes1 = ["attr_value": "firefox"] + let attributes2 = ["attr_value": false] + let attributes3 = ["attr_value": 1.5] + let attributes4 = ["attr_value": 10] + let attributes5 = ["attr_value": 10.0] var conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchStringType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchBoolType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchDecimalType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue3)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes3))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchIntType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue4)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes4))) conditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExactMatchIntType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue5)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes5))) } // MARK: - ExistsMatcher Tests func testExistsMatcherReturnsFalseWhenAttributeIsNotProvided() { - let attributesPassOrValue = [String: String]() + let attributes = [String: String]() let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExistsMatchType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testExistsMatcherReturnsFalseWhenAttributeIsNull() { - let attributesPassOrValue: [String: Any?] = ["attr_value": nil] + let attributes: [String: Any?] = ["attr_value": nil] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExistsMatchType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testExistsMatcherReturnsFalseWhenAttributeIsNSNull() { - let attributesPassOrValue: [String: Any?] = ["attr_value": NSNull()] + let attributes: [String: Any?] = ["attr_value": NSNull()] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExistsMatchType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testExistsMatcherReturnsTrueWhenAttributeValueIsProvided() { - let attributesPassOrValue1 = ["attr_value": ""] - let attributesPassOrValue2 = ["attr_value": "iPhone"] - let attributesPassOrValue3 = ["attr_value": 10] - let attributesPassOrValue4 = ["attr_value": 10.5] - let attributesPassOrValue5 = ["attr_value": false] + let attributes1 = ["attr_value": ""] + let attributes2 = ["attr_value": "iPhone"] + let attributes3 = ["attr_value": 10] + let attributes4 = ["attr_value": 10.5] + let attributes5 = ["attr_value": false] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithExistsMatchType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue3)) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue4)) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue5)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes3))) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes4))) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes5))) } // MARK: - SubstringMatcher Tests @@ -399,125 +399,125 @@ class AudienceTests_Evaluate: XCTestCase { "type": "custom_attribute", "match": "substring"] - let attributesPassOrValue = ["device_type": "iPhone"] + let attributes = ["device_type": "iPhone"] let userAttribute: ConditionHolder = try! OTUtils.model(from: condition) - XCTAssertNil(try? userAttribute.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertNil(try? userAttribute.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testSubstringMatcherReturnsFalseWhenConditionValueIsNotSubstringOfUserValue() { - let attributesPassOrValue = ["attr_value": "Breaking news!"] + let attributes = ["attr_value": "Breaking news!"] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithSubstringMatchType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testSubstringMatcherReturnsTrueWhenConditionValueIsSubstringOfUserValue() { - let attributesPassOrValue1 = ["attr_value": "firefox"] - let attributesPassOrValue2 = ["attr_value": "chrome vs firefox"] + let attributes1 = ["attr_value": "firefox"] + let attributes2 = ["attr_value": "chrome vs firefox"] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithSubstringMatchType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) } func testSubstringMatcherReturnsNullWhenAttributeValueIsNotAString() { - let attributesPassOrValue1 = ["attr_value": 10.5] - let attributesPassOrValue2: [String: Any?] = ["attr_value": nil] + let attributes1 = ["attr_value": 10.5] + let attributes2: [String: Any?] = ["attr_value": nil] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithSubstringMatchType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) } func testSubstringMatcherReturnsNullWhenAttributeIsNotProvided() { - let attributesPassOrValue1: [String: Any] = [:] - let attributesPassOrValue2: [String: Any]? = nil + let attributes1: [String: Any] = [:] + let attributes2: [String: Any]? = nil let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithSubstringMatchType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) } // MARK: - GTMatcher Tests func testGTMatcherReturnsFalseWhenAttributeValueIsLessThanOrEqualToConditionValue() { - let attributesPassOrValue1 = ["attr_value": 5] - let attributesPassOrValue2 = ["attr_value": 10] - let attributesPassOrValue3 = ["attr_value": 10.0] + let attributes1 = ["attr_value": 5] + let attributes2 = ["attr_value": 10] + let attributes3 = ["attr_value": 10.0] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithGreaterThanMatchType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue3)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes3))) } func testGTMatcherReturnsNullWhenAttributeValueIsNotANumericValue() { - let attributesPassOrValue1 = ["attr_value": "invalid"] - let attributesPassOrValue2 = [String: String]() - let attributesPassOrValue3 = ["attr_value": true] - let attributesPassOrValue4 = ["attr_value": false] + let attributes1 = ["attr_value": "invalid"] + let attributes2 = [String: String]() + let attributes3 = ["attr_value": true] + let attributes4 = ["attr_value": false] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithGreaterThanMatchType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue3)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue4)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes3))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes4))) } func testGTMatcherReturnsNullWhenAttributeValueIsInfinity() { - let attributesPassOrValue = ["attr_value": Double.infinity] + let attributes = ["attr_value": Double.infinity] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithGreaterThanMatchType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testGTMatcherReturnsTrueWhenAttributeValueIsGreaterThanConditionValue() { - let attributesPassOrValue1 = ["attr_value": 15] - let attributesPassOrValue2 = ["attr_value": 10.1] + let attributes1 = ["attr_value": 15] + let attributes2 = ["attr_value": 10.1] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithGreaterThanMatchType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) } // MARK: - LTMatcher Tests func testLTMatcherReturnsFalseWhenAttributeValueIsGreaterThanOrEqualToConditionValue() { - let attributesPassOrValue1 = ["attr_value": 15] - let attributesPassOrValue2 = ["attr_value": 10] + let attributes1 = ["attr_value": 15] + let attributes2 = ["attr_value": 10] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithLessThanMatchType) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertFalse(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertFalse(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) } func testLTMatcherReturnsNullWhenAttributeValueIsNotANumericValue() { - let attributesPassOrValue1 = ["attr_value": "invalid"] - let attributesPassOrValue2 = [String: String]() - let attributesPassOrValue3 = ["attr_value": true] - let attributesPassOrValue4 = ["attr_value": false] + let attributes1 = ["attr_value": "invalid"] + let attributes2 = [String: String]() + let attributes3 = ["attr_value": true] + let attributes4 = ["attr_value": false] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithLessThanMatchType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue3)) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue4)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes3))) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes4))) } func testLTMatcherReturnsNullWhenAttributeValueIsInfinity() { - let attributesPassOrValue = ["attr_value": Double.infinity] + let attributes = ["attr_value": Double.infinity] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithLessThanMatchType) - XCTAssertNil(try? conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue)) + XCTAssertNil(try? conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes))) } func testLTMatcherReturnsTrueWhenAttributeValueIsLessThanConditionValue() { - let attributesPassOrValue1 = ["attr_value": 5] - let attributesPassOrValue2 = ["attr_value": 9.9] + let attributes1 = ["attr_value": 5] + let attributes2 = ["attr_value": 9.9] let conditionHolder: ConditionHolder = try! OTUtils.model(from: kAudienceConditionsWithLessThanMatchType) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue1)) - XCTAssertTrue(try! conditionHolder.evaluate(project: nil, attributes: attributesPassOrValue2)) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes1))) + XCTAssertTrue(try! conditionHolder.evaluate(project: nil, user: OTUtils.user(attributes: attributes2))) } } diff --git a/Tests/OptimizelyTests-DataModel/ConditionHolderTests.swift b/Tests/OptimizelyTests-DataModel/ConditionHolderTests.swift index b2d51276..266a5bdb 100644 --- a/Tests/OptimizelyTests-DataModel/ConditionHolderTests.swift +++ b/Tests/OptimizelyTests-DataModel/ConditionHolderTests.swift @@ -129,7 +129,7 @@ class ConditionHolderTests: XCTestCase { XCTAssertNotNil(data) let testHolder = try? JSONDecoder().decode(ConditionHolder.self, from: data!) - let bool = try? testHolder!.evaluate(project: nil, attributes: nil) + let bool = try? testHolder!.evaluate(project: nil, user: OTUtils.user()) XCTAssertNil(bool) } } @@ -299,7 +299,7 @@ extension ConditionHolderTests { func testOperaterOnEmptyConditionArray() { let array: [ConditionHolder] = [] - let result = try? array.evaluate(op: .and, project: nil, attributes: nil) + let result = try? array.evaluate(op: .and, project: nil, user: OTUtils.user()) XCTAssertTrue(result == nil) } diff --git a/Tests/OptimizelyTests-DataModel/ConditionHolderTests_Evaluate.swift b/Tests/OptimizelyTests-DataModel/ConditionHolderTests_Evaluate.swift index 5d3a495c..e11efd41 100644 --- a/Tests/OptimizelyTests-DataModel/ConditionHolderTests_Evaluate.swift +++ b/Tests/OptimizelyTests-DataModel/ConditionHolderTests_Evaluate.swift @@ -42,37 +42,37 @@ class ConditionHolderTests_Evaluate: XCTestCase { func testEvaluate_I() { let model = ConditionHolder.leaf(.audienceId("11111")) - XCTAssertTrue(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertTrue(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_A() { let model = ConditionHolder.logicalOp(.and) - XCTAssertNil(try? model.evaluate(project: project, attributes: attributeData)) + XCTAssertNil(try? model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_O() { let model = ConditionHolder.logicalOp(.or) - XCTAssertNil(try? model.evaluate(project: project, attributes: attributeData)) + XCTAssertNil(try? model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_N() { let model = ConditionHolder.logicalOp(.not) - XCTAssertNil(try? model.evaluate(project: project, attributes: attributeData)) + XCTAssertNil(try? model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_AinArray() { let model = ConditionHolder.array([.logicalOp(.and)]) - XCTAssertNil(try? model.evaluate(project: project, attributes: attributeData)) + XCTAssertNil(try? model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_OinArray() { let model = ConditionHolder.array([.logicalOp(.or)]) - XCTAssertFalse(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertFalse(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_NinArray() { let model = ConditionHolder.array([.logicalOp(.not)]) - XCTAssertNil(try? model.evaluate(project: project, attributes: attributeData)) + XCTAssertNil(try? model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_AI() { @@ -80,7 +80,7 @@ class ConditionHolderTests_Evaluate: XCTestCase { .logicalOp(.and), .leaf(.audienceId("11111"))] ) - XCTAssertTrue(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertTrue(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_OI() { @@ -88,7 +88,7 @@ class ConditionHolderTests_Evaluate: XCTestCase { .logicalOp(.or), .leaf(.audienceId("11111"))] ) - XCTAssertTrue(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertTrue(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_NI() { @@ -96,7 +96,7 @@ class ConditionHolderTests_Evaluate: XCTestCase { .logicalOp(.not), .leaf(.audienceId("11111"))] ) - XCTAssertFalse(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertFalse(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_A_AI() { @@ -105,7 +105,7 @@ class ConditionHolderTests_Evaluate: XCTestCase { .array([.logicalOp(.and), .leaf(.audienceId("11111"))]) ]) - XCTAssertTrue(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertTrue(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_A_OI() { @@ -114,7 +114,7 @@ class ConditionHolderTests_Evaluate: XCTestCase { .array([.logicalOp(.or), .leaf(.audienceId("11111"))]) ]) - XCTAssertTrue(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertTrue(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_A_NI() { @@ -123,7 +123,7 @@ class ConditionHolderTests_Evaluate: XCTestCase { .array([.logicalOp(.not), .leaf(.audienceId("11111"))]) ]) - XCTAssertFalse(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertFalse(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_A_I_AII() { @@ -134,7 +134,7 @@ class ConditionHolderTests_Evaluate: XCTestCase { .leaf(.audienceId("33333")), .leaf(.audienceId("44444"))]) ]) - XCTAssertFalse(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertFalse(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_O__A_I_OII__O_AII_NI() { @@ -152,7 +152,7 @@ class ConditionHolderTests_Evaluate: XCTestCase { .array([.logicalOp(.not), .leaf(.audienceId("66666"))])]) ]) - XCTAssertTrue(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertTrue(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } } @@ -163,17 +163,17 @@ extension ConditionHolderTests_Evaluate { func testEvaluate_U() { let model: ConditionHolder = try! OTUtils.model(from: userAttributeData) - XCTAssertTrue(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertTrue(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_AU() { let model: ConditionHolder = try! OTUtils.model(from: ["and", userAttributeData]) - XCTAssertTrue(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertTrue(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } func testEvaluate_NU() { let model: ConditionHolder = try! OTUtils.model(from: ["not", userAttributeData]) - XCTAssertFalse(try! model.evaluate(project: project, attributes: attributeData)) + XCTAssertFalse(try! model.evaluate(project: project, user: OTUtils.user(attributes: attributeData))) } } @@ -185,7 +185,7 @@ extension ConditionHolderTests_Evaluate { func testEvaluate_Empty() { let model: ConditionHolder = try! OTUtils.model(from: []) do { - _ = try model.evaluate(project: project, attributes: attributeData) + _ = try model.evaluate(project: project, user: OTUtils.user(attributes: attributeData)) XCTAssert(false, "cannot evaluate empty condtion") } catch { XCTAssert(true) @@ -195,7 +195,7 @@ extension ConditionHolderTests_Evaluate { func testEvaluate_InvalidFirstItem() { let model: ConditionHolder = try! OTUtils.model(from: [["and", userAttributeData]]) do { - _ = try model.evaluate(project: project, attributes: attributeData) + _ = try model.evaluate(project: project, user: OTUtils.user(attributes: attributeData)) XCTAssert(false, "only operator or leaf node can be the first item") } catch { XCTAssert(true) @@ -205,7 +205,7 @@ extension ConditionHolderTests_Evaluate { func testEvaluate_OperatorOnly() { let model: ConditionHolder = try! OTUtils.model(from: ["and"]) do { - _ = try model.evaluate(project: project, attributes: attributeData) + _ = try model.evaluate(project: project, user: OTUtils.user(attributes: attributeData)) XCTAssert(false, "only operator or leaf node can be the first item") } catch { XCTAssert(true) diff --git a/Tests/OptimizelyTests-DataModel/ConditionLeafTests.swift b/Tests/OptimizelyTests-DataModel/ConditionLeafTests.swift index 8d67b63f..a1a0930b 100644 --- a/Tests/OptimizelyTests-DataModel/ConditionLeafTests.swift +++ b/Tests/OptimizelyTests-DataModel/ConditionLeafTests.swift @@ -53,7 +53,7 @@ class ConditionLeafTests: XCTestCase { let model: [ConditionLeaf] = try! OTUtils.model(from: [audienceId]) var tmpError: Error? do { - let _ = try model[0].evaluate(project: nil, attributes: ["country": "us"]) + let _ = try model[0].evaluate(project: nil, user: OTUtils.user(attributes: ["country": "us"])) } catch { tmpError = error } diff --git a/Tests/OptimizelyTests-DataModel/IntegrationTests.swift b/Tests/OptimizelyTests-DataModel/IntegrationTests.swift new file mode 100644 index 00000000..39cdbd58 --- /dev/null +++ b/Tests/OptimizelyTests-DataModel/IntegrationTests.swift @@ -0,0 +1,94 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +// MARK: - Sample Data + +class IntegrationTests: XCTestCase { + static var sampleData: [String: Any] = ["key": "partner", + "host": "https://google.com", + "publicKey": "abc123"] +} + +// MARK: - Decode + +extension IntegrationTests { + + func testDecodeSuccessWithJSONValid() { + let data: [String: Any] = ["key": "partner", + "host": "https://google.com", + "publicKey": "abc123"] + let model: Integration = try! OTUtils.model(from: data) + + XCTAssert(model.key == "partner") + XCTAssert(model.host == "https://google.com") + XCTAssert(model.publicKey == "abc123") + } + + func testDecodeSuccessWithJSONValid2() { + let data: [String: Any] = ["key": "partner", + "host": "https://google.com"] + let model: Integration = try! OTUtils.model(from: data) + + XCTAssert(model.key == "partner") + XCTAssert(model.host == "https://google.com") + XCTAssertNil(model.publicKey) + } + + func testDecodeSuccessWithJSONValid3() { + let data: [String: Any] = ["key": "partner"] + let model: Integration = try! OTUtils.model(from: data) + + XCTAssert(model.key == "partner") + XCTAssertNil(model.host) + XCTAssertNil(model.publicKey) + } + + func testDecodeSuccessWithJSONValid4() { + let data: [String: Any] = ["key": "partner", "any-int": 10, "any-bool": true, "any-string": "any-str"] + let model: Integration = try! OTUtils.model(from: data) + + XCTAssert(model.key == "partner") + XCTAssertNil(model.host) + XCTAssertNil(model.publicKey) + } + + func testDecodeFailWithMissingKey() { + let data: [String: Any] = ["host": "https://google.com"] + let model: Integration? = try? OTUtils.model(from: data) + XCTAssertNil(model) + } + + func testDecodeFailWithJSONEmpty() { + let data: [String: Any] = [:] + let model: Integration? = try? OTUtils.model(from: data) + XCTAssertNil(model) + } +} + +// MARK: - Encode + +extension IntegrationTests { + + func testEncodeJSON() { + let model = Integration(key: "key", + host: "host", + publicKey: "public-key") + XCTAssert(OTUtils.isEqualWithEncodeThenDecode(model)) + } + +} diff --git a/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift b/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift index afeb0a0b..aebde55f 100644 --- a/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift +++ b/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift @@ -97,6 +97,18 @@ class ProjectConfigTests: XCTestCase { XCTAssertEqual(variations3, []) } + func testAllSegments() { + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + let optimizely = OptimizelyClient(sdkKey: "12345", + userProfileService: OTUtils.createClearUserProfileService()) + try! optimizely.start(datafile: datafile) + let segments = optimizely.config!.allSegments + XCTAssertEqual(3, segments.count, "redundant items should be filtered out") + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segments)) + XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", optimizely.config!.publicKeyForODP) + XCTAssertEqual("https://api.zaius.com", optimizely.config!.hostForODP) + } + } // MARK: - Others diff --git a/Tests/OptimizelyTests-DataModel/ProjectTests.swift b/Tests/OptimizelyTests-DataModel/ProjectTests.swift index d696c6a5..dbf5ba4c 100644 --- a/Tests/OptimizelyTests-DataModel/ProjectTests.swift +++ b/Tests/OptimizelyTests-DataModel/ProjectTests.swift @@ -31,6 +31,7 @@ class ProjectTests: XCTestCase { "anonymizeIP": true, "rollouts": [RolloutTests.sampleData], "typedAudiences": [AudienceTests.sampleData], + "integrations": [IntegrationTests.sampleData], "featureFlags": [FeatureFlagTests.sampleData], "botFiltering": false, "sendFlagDecisions": true] @@ -57,7 +58,8 @@ extension ProjectTests { XCTAssert(model.anonymizeIP == true) XCTAssert(model.rollouts == [try! OTUtils.model(from: RolloutTests.sampleData)]) XCTAssert(model.typedAudiences == [try! OTUtils.model(from: AudienceTests.sampleData)]) - XCTAssert(model.featureFlags == [try OTUtils.model(from: FeatureFlagTests.sampleData)]) + XCTAssert(model.integrations == [try! OTUtils.model(from: IntegrationTests.sampleData)]) + XCTAssert(model.featureFlags == [try! OTUtils.model(from: FeatureFlagTests.sampleData)]) XCTAssert(model.botFiltering == false) XCTAssert(model.sendFlagDecisions == true) XCTAssert(model.sdkKey == nil) @@ -178,6 +180,16 @@ extension ProjectTests { let model: Project = try! OTUtils.model(from: data) XCTAssert(model.projectId == "11111") + XCTAssertNil(model.typedAudiences) + } + + func testDecodeSuccessWithMissingIntegrations() { + var data: [String: Any] = ProjectTests.sampleData + data["integrations"] = nil + + let model: Project = try! OTUtils.model(from: data) + XCTAssert(model.projectId == "11111") + XCTAssertNil(model.integrations) } func testDecodeSuccessWithMissingBotFiltering() { @@ -186,6 +198,7 @@ extension ProjectTests { let model: Project = try! OTUtils.model(from: data) XCTAssert(model.projectId == "11111") + XCTAssertNil(model.botFiltering) } func testDecodeSuccessWithMissingSendFlagDecisions() { @@ -194,6 +207,7 @@ extension ProjectTests { let model: Project = try! OTUtils.model(from: data) XCTAssert(model.projectId == "11111") + XCTAssertNil(model.sendFlagDecisions) } } diff --git a/Tests/OptimizelyTests-DataModel/UserAttributeTests.swift b/Tests/OptimizelyTests-DataModel/UserAttributeTests.swift index a7cd8e87..e0ba6382 100644 --- a/Tests/OptimizelyTests-DataModel/UserAttributeTests.swift +++ b/Tests/OptimizelyTests-DataModel/UserAttributeTests.swift @@ -183,7 +183,7 @@ extension UserAttributeTests { let model: UserAttribute = try! OTUtils.model(from: json) var tmpError: Error? do { - _ = try model.evaluate(attributes: ["country": "us"]) + _ = try model.evaluate(user: OTUtils.user(attributes: ["country": "us"])) } catch { tmpError = error } @@ -195,7 +195,7 @@ extension UserAttributeTests { let model: UserAttribute = try! OTUtils.model(from: json) var tmpError: Error? do { - _ = try model.evaluate(attributes: ["name": "geo"]) + _ = try model.evaluate(user: OTUtils.user(attributes: ["name": "geo"])) } catch { tmpError = error } diff --git a/Tests/OptimizelyTests-DataModel/UserAttributeTests_Evaluate.swift b/Tests/OptimizelyTests-DataModel/UserAttributeTests_Evaluate.swift index 98c01713..428eb6ab 100644 --- a/Tests/OptimizelyTests-DataModel/UserAttributeTests_Evaluate.swift +++ b/Tests/OptimizelyTests-DataModel/UserAttributeTests_Evaluate.swift @@ -24,7 +24,7 @@ class UserAttributeTests_Evaluate: XCTestCase { var err: Error? let model = UserAttribute(name: "country", type: "unknown", match: "exact", value: .string("us")) do { - try _ = model.evaluate(attributes: ["":""]) + try _ = model.evaluate(user: OTUtils.user(attributes: ["":""])) } catch { err = error } @@ -35,7 +35,7 @@ class UserAttributeTests_Evaluate: XCTestCase { var err: Error? let model = UserAttribute(name: "country", type: "custom_attribute", match: "unknown", value: .string("us")) do { - try _ = model.evaluate(attributes: ["":""]) + try _ = model.evaluate(user: OTUtils.user(attributes: ["":""])) } catch { err = error } @@ -47,7 +47,7 @@ class UserAttributeTests_Evaluate: XCTestCase { var model = UserAttribute(name: "", type: "custom_attribute", match: "exact", value: .string("us")) model.name = nil do { - try _ = model.evaluate(attributes: ["":""]) + try _ = model.evaluate(user: OTUtils.user(attributes: ["":""])) } catch { err = error } @@ -60,7 +60,7 @@ class UserAttributeTests_Evaluate: XCTestCase { let name = "country" let model = UserAttribute(name: name, type: "custom_attribute", match: "exact", value: .string("us")) do { - try _ = model.evaluate(attributes: attributes) + try _ = model.evaluate(user: OTUtils.user(attributes: attributes)) } catch { err = error } @@ -73,7 +73,7 @@ class UserAttributeTests_Evaluate: XCTestCase { let name = "country" let model = UserAttribute(name: name, type: "custom_attribute", match: "exact", value: nil) do { - try _ = model.evaluate(attributes: attributes) + try _ = model.evaluate(user: OTUtils.user(attributes: attributes)) } catch { err = error } @@ -86,7 +86,7 @@ class UserAttributeTests_Evaluate: XCTestCase { let name = "country" let model = UserAttribute(name: name, type: "custom_attribute", match: "exact", value: .string("us")) do { - try _ = model.evaluate(attributes: attributes) + try _ = model.evaluate(user: OTUtils.user(attributes: attributes)) } catch { err = error } @@ -98,73 +98,73 @@ class UserAttributeTests_Evaluate: XCTestCase { func testEvaluateExactString() { let attributes = ["country": "us"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .string("us")) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactInt() { let attributes = ["country": 100] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .int(100)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactDouble() { let attributes = ["country": 15.3] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .double(15.3)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactBool() { let attributes = ["country": true] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .bool(true)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactBool2() { let attributes = ["country": false] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .bool(false)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactStringFalse() { let attributes = ["country": "ca"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .string("us")) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactIntFalse() { let attributes = ["country": 200] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .int(100)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactDoubleFalse() { let attributes = ["country": 15.4] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .double(15.3)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactBoolFalse() { let attributes = ["country": false] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .bool(true)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactDifferentTypeNil() { let attributes = ["country": "us"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .int(100)) - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactSameValueButDifferentName() { let attributes = ["address": "us"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .string("us")) - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateExactSameValueButDifferentName2() { let attributes = ["country": "ca", "address": "us"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exact", value: .string("us")) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } } @@ -175,31 +175,31 @@ extension UserAttributeTests_Evaluate { func testEvaluateSubstring() { let attributes = ["country": "us-gb"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "substring", value: .string("us")) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateSubstringFalse() { let attributes = ["country": "gb-ca"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "substring", value: .string("us")) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateSubstringReverseFalse() { let attributes = ["country": "us"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "substring", value: .string("us-ca")) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateSubstringDifferentTypeNil() { let attributes = ["country": "us"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "substring", value: .int(100)) - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEvaluateSubstringMissingAttributeNil() { let attributes = ["country": "us"] let model = UserAttribute(name: "h", type: "custom_attribute", match: "substring", value: .string("us")) - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) } } @@ -211,13 +211,13 @@ extension UserAttributeTests_Evaluate { func testExist() { let attributes = ["country": "us"] let model = UserAttribute(name: "country", type: "custom_attribute", match: "exists", value: .string("ca")) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testExistFail() { let attributes = ["country": "us"] let model = UserAttribute(name: "h", type: "custom_attribute", match: "exists", value: .string("us")) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } } @@ -229,55 +229,55 @@ extension UserAttributeTests_Evaluate { func testGreaterThanIntToInt() { let attributes = ["country": 100] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .int(50)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanIntToDouble() { let attributes = ["country": 100] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .double(51.3)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanIntToString() { let attributes = ["country": 100] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .string("us")) - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanIntToBool() { let attributes = ["country": 100] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .bool(true)) - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanDoubleToInt() { let attributes = ["country": 101.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .int(50)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanDoubleToDouble() { let attributes = ["country": 101.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .double(51.3)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanDoubleToIntFail() { let attributes = ["country": 101.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .int(200)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanDoubleToDoubleFail() { let attributes = ["country": 101.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .double(201.2)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanDoubleToDoubleEqualFail() { let attributes = ["country": 101.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "gt", value: .double(101.2)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } } @@ -289,21 +289,21 @@ extension UserAttributeTests_Evaluate { func testGreaterThanOrEqualIntToInt() { var attributes = ["country": 50] let model = UserAttribute(name: "country", type: "custom_attribute", match: "ge", value: .int(50)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["country"] = 51 - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["country"] = 49 - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanOrEqualDoubleToDouble() { var attributes = ["country": 100.0] let model = UserAttribute(name: "country", type: "custom_attribute", match: "ge", value: .double(51.3)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["country"] = 51.3 - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["country"] = 51.0 - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } } @@ -314,55 +314,55 @@ extension UserAttributeTests_Evaluate { func testLessThanIntToInt() { let attributes = ["country": 10] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .int(50)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanIntToDouble() { let attributes = ["country": 10] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .double(51.3)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanIntToString() { let attributes = ["country": 100] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .string("us")) - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanIntToBool() { let attributes = ["country": 100] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .bool(true)) - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanDoubleToInt() { let attributes = ["country": 11.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .int(50)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanDoubleToDouble() { let attributes = ["country": 11.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .double(51.3)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanDoubleToIntFail() { let attributes = ["country": 101.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .int(20)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanDoubleToDoubleFail() { let attributes = ["country": 101.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .double(21.2)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanDoubleToDoubleEqualFail() { let attributes = ["country": 101.2] let model = UserAttribute(name: "country", type: "custom_attribute", match: "lt", value: .double(101.2)) - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } } @@ -373,21 +373,21 @@ extension UserAttributeTests_Evaluate { func testLessThanOrEqualIntToInt() { var attributes = ["country": 50] let model = UserAttribute(name: "country", type: "custom_attribute", match: "le", value: .int(50)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["country"] = 49 - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["country"] = 51 - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanOrEqualDoubleToDouble() { var attributes = ["country": 25.0] let model = UserAttribute(name: "country", type: "custom_attribute", match: "le", value: .double(51.3)) - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["country"] = 51.3 - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["country"] = 52.0 - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } } @@ -399,64 +399,102 @@ extension UserAttributeTests_Evaluate { let model = UserAttribute(name: "version", type: "custom_attribute", match: "semver_le", value: .string("2.0")) var attributes = ["version": "2.0.0"] - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "1.9" - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "2.5.1" - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanOrEqualSemanticVersion() { let model = UserAttribute(name: "version", type: "custom_attribute", match: "semver_ge", value: .string("2.0")) var attributes = ["version": "2.0.0"] - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "2.9" - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "1.9" - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testLessThanSemanticVersion() { let model = UserAttribute(name: "version", type: "custom_attribute", match: "semver_lt", value: .string("2.0")) var attributes = ["version": "2.0.0"] - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "1.9" - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "2.5.1" - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testGreaterThanSemanticVersion() { let model = UserAttribute(name: "version", type: "custom_attribute", match: "semver_gt", value: .string("2.0")) var attributes = ["version": "2.0.0"] - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "2.9" - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "1.9" - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEqualSemanticVersion() { let model = UserAttribute(name: "version", type: "custom_attribute", match: "semver_eq", value: .string("2.0")) var attributes = ["version": "2.0.0"] - XCTAssertTrue(try! model.evaluate(attributes: attributes)) + XCTAssertTrue(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "2.9" - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = "1.9" - XCTAssertFalse(try! model.evaluate(attributes: attributes)) + XCTAssertFalse(try! model.evaluate(user: OTUtils.user(attributes: attributes))) } func testEqualSemanticVersionInvalidType() { let model = UserAttribute(name: "version", type: "custom_attribute", match: "semver_eq", value: .string("2.0")) var attributes:[String: Any] = ["version": true] - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) attributes["version"] = 37 - XCTAssertNil(try? model.evaluate(attributes: attributes)) + XCTAssertNil(try? model.evaluate(user: OTUtils.user(attributes: attributes))) + } + +} + +// MARK: - Evaluate (Qualified) + +extension UserAttributeTests_Evaluate { + + func testEvaluateQualifiedTrue() { + let user = OTUtils.user() + user.qualifiedSegments = ["us", "th"] + + let model = UserAttribute(name: "odp.audiences", type: "third_party_dimension", match: "qualified", value: .string("us")) + XCTAssertTrue(try! model.evaluate(user: user)) + } + + func testEvaluateQualifiedFalse() { + let user = OTUtils.user() + user.qualifiedSegments = ["th"] + + let model = UserAttribute(name: "odp.audiences", type: "third_party_dimension", match: "qualified", value: .string("us")) + XCTAssertFalse(try! model.evaluate(user: user)) + } + + func testEvaluateQualified_ignoreName() { + let user = OTUtils.user() + user.qualifiedSegments = ["us", "th"] + + let model = UserAttribute(name: "partner-a", type: "third_party_dimension", match: "qualified", value: .string("us")) + XCTAssertTrue(try! model.evaluate(user: user), "name should be ignored for forward compatibility") + } + + func testEvaluateQualifiedInvalidValueType() { + let user = OTUtils.user() + user.qualifiedSegments = ["us", "th"] + + let model = UserAttribute(name: "partner-a", type: "third_party_dimension", match: "qualified", value: .int(100)) + XCTAssertNil(try? model.evaluate(user: user), "value must be string") } } diff --git a/Tests/TestData/odp/decide_audience_segments.json b/Tests/TestData/odp/decide_audience_segments.json new file mode 100644 index 00000000..ea463cf4 --- /dev/null +++ b/Tests/TestData/odp/decide_audience_segments.json @@ -0,0 +1,207 @@ +{ + "version": "4", + "sendFlagDecisions": true, + "rollouts": [ + { + "experiments": [ + { + "audienceIds": ["13389130056"], + "forcedVariations": {}, + "id": "3332020515", + "key": "rollout-rule-1", + "layerId": "3319450668", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 10000, + "entityId": "3324490633" + } + ], + "variations": [ + { + "featureEnabled": true, + "id": "3324490633", + "key": "rollout-variation-on", + "variables": [] + } + ] + }, + { + "audienceIds": [], + "forcedVariations": {}, + "id": "3332020556", + "key": "rollout-rule-2", + "layerId": "3319450668", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 10000, + "entityId": "3324490644" + } + ], + "variations": [ + { + "featureEnabled": false, + "id": "3324490644", + "key": "rollout-variation-off", + "variables": [] + } + ] + } + ], + "id": "3319450668" + } + ], + "anonymizeIP": true, + "botFiltering": true, + "projectId": "10431130345", + "variables": [], + "featureFlags": [ + { + "experimentIds": ["10390977673"], + "id": "4482920077", + "key": "flag-segment", + "rolloutId": "3319450668", + "variables": [ + { + "defaultValue": "42", + "id": "2687470095", + "key": "i_42", + "type": "integer" + } + ] + } + ], + "experiments": [ + { + "status": "Running", + "key": "experiment-segment", + "layerId": "10420273888", + "trafficAllocation": [ + { + "entityId": "10389729780", + "endOfRange": 10000 + } + ], + "audienceIds": ["$opt_dummy_audience"], + "audienceConditions": ["or", "13389142234", "13389141123"], + "variations": [ + { + "variables": [], + "featureEnabled": true, + "id": "10389729780", + "key": "variation-a" + }, + { + "variables": [], + "id": "10416523121", + "key": "variation-b" + } + ], + "forcedVariations": {}, + "id": "10390977673" + } + ], + "groups": [], + "integrations": [ + { + "key": "odp", + "host": "https://api.zaius.com", + "publicKey": "W4WzcEs-ABgXorzY7h1LCQ" + } + ], + "typedAudiences": [ + { + "id": "13389142234", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-audience-1" + }, + { + "id": "13389130056", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "us", + "type": "custom_attribute", + "name": "country", + "match": "exact" + } + ], + [ + "or", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-audience-2" + }, + { + "id": "13389130077", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-audience-3" + } + ], + "audiences": [ + { + "id": "13389141123", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"custom_attribute\", \"value\": 20}]]]", + "name": "adult" + } + ], + "attributes": [ + { + "id": "10401066117", + "key": "gender" + }, + { + "id": "10401066170", + "key": "testvar" + } + ], + "accountId": "10367498574", + "events": [], + "revision": "101" +} diff --git a/Tests/TestData/odp/odp_integrated_no_segments.json b/Tests/TestData/odp/odp_integrated_no_segments.json new file mode 100644 index 00000000..a345ca1f --- /dev/null +++ b/Tests/TestData/odp/odp_integrated_no_segments.json @@ -0,0 +1,22 @@ +{ + "version": "4", + "rollouts": [], + "anonymizeIP": true, + "projectId": "10431130345", + "variables": [], + "featureFlags": [], + "experiments": [], + "audiences": [], + "groups": [], + "attributes": [], + "accountId": "10367498574", + "events": [], + "integrations": [ + { + "key": "odp", + "host": "https://api.zaius.com", + "publicKey": "W4WzcEs-ABgXorzY7h1LCQ" + } + ], + "revision": "100" +} diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index b22cdc5a..5f989f84 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -87,6 +87,16 @@ class OTUtils { return nil } + static func compareDictionaries(_ d1: [String: Any], _ d2: [String: Any]) -> Bool { + if #available(iOS 11.0, tvOS 11.0, *) { + let data1 = try! JSONSerialization.data(withJSONObject: d1, options: .sortedKeys) + let data2 = try! JSONSerialization.data(withJSONObject: d2, options: .sortedKeys) + return data1 == data2 + } else { + return true + } + } + static func model(from raw: Any) throws -> T { return try JSONDecoder().decode(T.self, from: jsonDataFromNative(raw)) } @@ -223,8 +233,9 @@ class OTUtils { } static func clearAllEventQueues() { - removeAllFiles(including: "OPTEventQueue", in: .documentDirectory) - removeAllFiles(including: "OPTEventQueue", in: .cachesDirectory) + // clear all FS + ODP event queues + removeAllFiles(including: "OPTEvent", in: .documentDirectory) + removeAllFiles(including: "OPTEvent", in: .cachesDirectory) } // MARK: - datafiles @@ -310,6 +321,14 @@ class OTUtils { UserDefaults.standard.synchronize() } + // MARK: - decide + + static func user(userId: String? = nil, attributes: [String: Any?]? = nil) -> OptimizelyUserContext { + return OptimizelyUserContext(optimizely: OptimizelyClient(sdkKey: "any-key"), + userId: userId ?? "any-user", + attributes: attributes) + } + // MARK: - concurrency static func runConcurrent(for items: [String],