From 60f0a2db2eb6c3df0eb7f40351c698e1c6678fe2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 12 Jan 2022 16:15:11 -0800 Subject: [PATCH 01/84] wip --- DemoSwiftApp/AppDelegate.swift | 64 ++++-- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 182 ++++++++++++++++++ .../DefaultAudienceSegmentsHandler.swift | 36 ++++ .../OPTAudienceSegmentsHandler.swift | 26 +++ .../OptimizelyUserContext+ODP.swift | 46 +++++ .../Audience Targeting/SegmentsCache.swift | 30 +++ .../Audience Targeting/ZaiusApiManager.swift | 158 +++++++++++++++ .../OptimizelyUserContext.swift | 3 + Sources/Optimizely/OptimizelyClient.swift | 3 + 9 files changed, 528 insertions(+), 20 deletions(-) create mode 100644 Sources/Audience Targeting/DefaultAudienceSegmentsHandler.swift create mode 100644 Sources/Audience Targeting/OPTAudienceSegmentsHandler.swift create mode 100644 Sources/Audience Targeting/OptimizelyUserContext+ODP.swift create mode 100644 Sources/Audience Targeting/SegmentsCache.swift create mode 100644 Sources/Audience Targeting/ZaiusApiManager.swift diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 0a4c2823..3d5bdbc5 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -102,26 +102,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func initializeOptimizelySDKWithCustomization() { // customization example (optional) - // You can enable background datafile polling by setting periodicDownloadInterval (polling is disabled by default) - // 60 sec interval may be too frequent. This is for demo purpose. (You can set this to nil to use the recommended value of 600 secs). - let downloadIntervalInSecs: Int? = 60 - - // You can turn off event batching with 0 timerInterval (this means that events are sent out immediately to the server instead of saving in the local queue for batching) - let eventDispatcher = DefaultEventDispatcher(timerInterval: 0) - - // customize logger - let customLogger = CustomLogger() optimizely = OptimizelyClient(sdkKey: sdkKey, - logger: customLogger, - eventDispatcher: eventDispatcher, - periodicDownloadInterval: downloadIntervalInSecs, defaultLogLevel: logLevel) - addNotificationListeners() - - // initialize SDK - optimizely!.start { result in + optimizely.start { result in switch result { case .failure(let error): print("Optimizely SDK initiliazation failed: \(error)") @@ -130,12 +115,51 @@ 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) + let user = self.optimizely.createUserContext(userId: "tester") + user.fetchQualifiedSegments(apiKey: "valid-api-key") { success in + if success { + let decisions = user.decideAll(options: nil) + print("[ODP DECISIONS] \(decisions)") + } + } } + + +// // You can enable background datafile polling by setting periodicDownloadInterval (polling is disabled by default) +// // 60 sec interval may be too frequent. This is for demo purpose. (You can set this to nil to use the recommended value of 600 secs). +// let downloadIntervalInSecs: Int? = 60 +// +// // You can turn off event batching with 0 timerInterval (this means that events are sent out immediately to the server instead of saving in the local queue for batching) +// let eventDispatcher = DefaultEventDispatcher(timerInterval: 0) +// +// // customize logger +// let customLogger = CustomLogger() +// +// optimizely = OptimizelyClient(sdkKey: sdkKey, +// logger: customLogger, +// eventDispatcher: eventDispatcher, +// periodicDownloadInterval: downloadIntervalInSecs, +// defaultLogLevel: logLevel) +// +// addNotificationListeners() +// +// // initialize SDK +// optimizely!.start { result in +// switch result { +// case .failure(let error): +// print("Optimizely SDK initiliazation failed: \(error)") +// case .success: +// print("Optimizely SDK initialized successfully!") +// @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) +// } } func addNotificationListeners() { diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 84c56a6f..2b560bb8 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -625,6 +625,86 @@ 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 */; }; + 6E6522BA278DF26400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522BB278DF26400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522BC278DF26400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522BD278DF26400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522BE278DF27200954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522BF278DF27300954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C0278DF27300954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C1278DF27400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C2278DF27400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C3278DF27500954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C4278DF27600954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C5278DF27600954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C6278DF27700954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522C9278DF27900954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; + 6E6522CD278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522CE278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522CF278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D0278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D1278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D2278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D3278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D4278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D5278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D6278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D7278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D8278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522D9278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522DA278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522DB278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522DC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; + 6E6522DE278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522DF278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E0278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E1278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E2278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E3278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E4278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E5278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E6278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E7278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E8278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E9278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522EA278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522EF278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F0278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F1278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F2278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F3278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F4278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F5278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F6278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F7278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F8278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522F9278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522FA278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522FB278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522FC278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522FD278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522FE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E652300278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652301278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652302278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652303278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652304278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652305278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652306278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652307278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652308278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E652309278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E65230A278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E65230B278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E65230C278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E65230D278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E65230E278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; + 6E65230F278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.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 */; }; @@ -1945,6 +2025,11 @@ 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 = ""; }; + 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusApiManager.swift; sourceTree = ""; }; + 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPTAudienceSegmentsHandler.swift; sourceTree = ""; }; + 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAudienceSegmentsHandler.swift; sourceTree = ""; }; + 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OptimizelyUserContext+ODP.swift"; sourceTree = ""; }; + 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentsCache.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 = ""; }; @@ -2340,6 +2425,18 @@ path = "OptimizelyTests-MultiClients"; sourceTree = ""; }; + 6E6522B8278DF20F00954EA1 /* Audience Targeting */ = { + isa = PBXGroup; + children = ( + 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */, + 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */, + 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, + 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */, + 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */, + ); + path = "Audience Targeting"; + sourceTree = ""; + }; 6E6BE008237F547200FE8274 /* optimizelyConfig */ = { isa = PBXGroup; children = ( @@ -2352,6 +2449,7 @@ 6E75165D22C520D400B2B157 /* Sources */ = { isa = PBXGroup; children = ( + 6E6522B8278DF20F00954EA1 /* Audience Targeting */, 6E75166622C520D400B2B157 /* Optimizely */, 6EC6DD3F24ABF8180017D296 /* Optimizely+Decide */, 6E75165E22C520D400B2B157 /* Customization */, @@ -3041,6 +3139,8 @@ dependencies = ( ); name = "OptimizelySwiftSDK-iOS"; + packageProductDependencies = ( + ); productName = OptimizelySwiftSDKiOS; productReference = 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */; productType = "com.apple.product-type.framework"; @@ -3166,6 +3266,8 @@ Base, ); mainGroup = 0B7CB0B821AC5FE2007B77E5; + packageReferences = ( + ); productRefGroup = 0B7CB0C321AC5FE2007B77E5 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -3786,9 +3888,11 @@ C78CAFA824486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E14CD932423F9A700010234 /* Experiment.swift in Sources */, 6E14CD982423F9C300010234 /* BackgroundingCallbacks.swift in Sources */, + 6E6522E5278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E623F07253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5C2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E14CDA52423F9C300010234 /* MurmurHash3.swift in Sources */, + 6E6522D4278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E86CEA824FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E5D12222638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E14CD912423F9A700010234 /* TrafficAllocation.swift in Sources */, @@ -3822,13 +3926,16 @@ 6E14CD8A2423F9A100010234 /* ConditionHolder.swift in Sources */, 6E14CD9E2423F9C300010234 /* OPTBucketer.swift in Sources */, 6E14CD742423F97200010234 /* OptimizelyConfig.swift in Sources */, + 6E6522C1278DF27400954EA1 /* ZaiusApiManager.swift in Sources */, 6EF8DE1124BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E14CDA12423F9C300010234 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E994B3925A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E14CDA82423F9C300010234 /* AtomicProperty.swift in Sources */, 6E14CD7A2423F98D00010234 /* OPTUserProfileService.swift in Sources */, 6E14CDA32423F9C300010234 /* Constants.swift in Sources */, + 6E6522F6278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E4544B1270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 6E652307278E688B00954EA1 /* SegmentsCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3848,6 +3955,7 @@ 6E424CF326324B620081004A /* OPTDatafileHandler.swift in Sources */, 6E424CF426324B620081004A /* DecisionInfo.swift in Sources */, 6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */, + 6E6522D3278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */, 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */, @@ -3857,6 +3965,7 @@ 6E424CF926324B620081004A /* DecisionResponse.swift in Sources */, 6E424CFA26324B620081004A /* DataStoreMemory.swift in Sources */, 6E424CFB26324B620081004A /* DataStoreUserDefaults.swift in Sources */, + 6E6522E4278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */, 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */, 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */, @@ -3876,6 +3985,7 @@ 6E424D0926324B620081004A /* FeatureVariable.swift in Sources */, 6E6419DA2657059700C49555 /* NotificationCenterTests_MultiClients.swift in Sources */, 6E424D0A26324B620081004A /* Rollout.swift in Sources */, + 6E6522F5278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E2D5DAE26338CA00002077F /* AtomicDictionaryTests.swift in Sources */, 6E424D0B26324B620081004A /* Variation.swift in Sources */, 6E424D0C26324B620081004A /* TrafficAllocation.swift in Sources */, @@ -3885,6 +3995,7 @@ 6E424D1026324B620081004A /* Group.swift in Sources */, 6E424D1126324B620081004A /* Variable.swift in Sources */, 6E424D1226324B620081004A /* Attribute.swift in Sources */, + 6E6522C0278DF27300954EA1 /* ZaiusApiManager.swift in Sources */, 6E424D1326324B620081004A /* BackgroundingCallbacks.swift in Sources */, 6E424D1426324B620081004A /* OPTNotificationCenter.swift in Sources */, 6E424D5026324C4D0081004A /* OptimizelyDecideOption.swift in Sources */, @@ -3901,6 +4012,7 @@ 6E424D1C26324B620081004A /* Array+Extension.swift in Sources */, 6E424D7626324DBD0081004A /* AtomicArrayTests.swift in Sources */, 6E424CD326324B270081004A /* OptimizelyError.swift in Sources */, + 6E652306278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */, 6E424CD426324B270081004A /* OptimizelyLogLevel.swift in Sources */, 6EE5918E264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift in Sources */, @@ -3949,6 +4061,7 @@ 6E7518A122C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E994B3525A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E7516D722C520D400B2B157 /* OPTUserProfileService.swift in Sources */, + 6E6522CE278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75195522C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E75176722C520D400B2B157 /* Utils.swift in Sources */, 6E75180522C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -3958,6 +4071,7 @@ 6E75186522C520D400B2B157 /* Rollout.swift in Sources */, 6E86CEA324FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75190122C520D500B2B157 /* Attribute.swift in Sources */, + 6E6522BB278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, 6E7516B322C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E7517D522C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, @@ -3990,13 +4104,16 @@ 6E7516CB22C520D400B2B157 /* OPTLogger.swift in Sources */, 6E7517A322C520D400B2B157 /* Array+Extension.swift in Sources */, 6EC6DD4224ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, + 6E652301278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6E75193122C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75190D22C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75194922C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7516EF22C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75174F22C520D400B2B157 /* LogMessage.swift in Sources */, + 6E6522F0278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E75189522C520D400B2B157 /* Experiment.swift in Sources */, 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */, + 6E6522DF278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E7518D122C520D400B2B157 /* AttributeValue.swift in Sources */, 6E75182922C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75171F22C520D400B2B157 /* OptimizelyResult.swift in Sources */, @@ -4051,9 +4168,11 @@ C78CAFAD24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7516AE22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75195022C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, + 6E6522EA278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E623F0C253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF612445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516D222C520D400B2B157 /* OPTLogger.swift in Sources */, + 6E6522D9278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E86CEAC24FDC849005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E5D12272638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75186022C520D400B2B157 /* FeatureVariable.swift in Sources */, @@ -4087,13 +4206,16 @@ 6ECB60D2234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E75192C22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7517AA22C520D400B2B157 /* Array+Extension.swift in Sources */, + 6E6522C6278DF27700954EA1 /* ZaiusApiManager.swift in Sources */, 6EF8DE1624BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75186C22C520D400B2B157 /* Rollout.swift in Sources */, 6E994B3E25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75183C22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75194422C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75179222C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, + 6E6522FB278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E4544B6270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 6E65230C278E688B00954EA1 /* SegmentsCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4142,12 +4264,14 @@ 6E623F08253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75188C22C520D400B2B157 /* Project.swift in Sources */, 6ECB60CE234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, + 6E6522F7278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E86CEA624FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75172222C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E7517E422C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E9B11D722C548A200C22D81 /* OptimizelyErrorTests.swift in Sources */, C78CAF8624485029009FE876 /* OptimizelyClientTests_OptimizelyJSON_Objc.m in Sources */, 6E75194C22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, + 6E652308278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6E7517C022C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75183822C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75175222C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4165,6 +4289,7 @@ 6E7517FC22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E75178222C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7518A422C520D400B2B157 /* FeatureFlag.swift in Sources */, + 6E6522E6278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75185C22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E20050C26B4D28500278087 /* MockLogger.swift in Sources */, 6E75176A22C520D400B2B157 /* Utils.swift in Sources */, @@ -4176,6 +4301,7 @@ 6EF8DE1224BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E9B11DA22C548A200C22D81 /* OptimizelyClientTests_ObjcAPIs.m in Sources */, 6E75179A22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, + 6E6522C2278DF27400954EA1 /* ZaiusApiManager.swift in Sources */, 6E75182022C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E5AB69323F6130D007A82B1 /* OptimizelyClientTests_Init_Sync.swift in Sources */, 6E4544B2270E67C800F2CEBC /* NetworkReachability.swift in Sources */, @@ -4185,6 +4311,7 @@ 6E7518B022C520D400B2B157 /* Group.swift in Sources */, 6E75185022C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E7516CE22C520D400B2B157 /* OPTLogger.swift in Sources */, + 6E6522D5278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E9B11AB22C5489300C22D81 /* MockUrlSession.swift in Sources */, 6E75190422C520D500B2B157 /* Attribute.swift in Sources */, 6E75193422C520D500B2B157 /* OPTDataStore.swift in Sources */, @@ -4217,6 +4344,7 @@ 6E75193722C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75191322C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75172522C520D400B2B157 /* OptimizelyResult.swift in Sources */, + 6E6522FA278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E75173122C520D400B2B157 /* Constants.swift in Sources */, 6E75184722C520D400B2B157 /* Event.swift in Sources */, 6E994B3D25A3E6EA00999262 /* DecisionResponse.swift in Sources */, @@ -4227,11 +4355,13 @@ 6E75189B22C520D400B2B157 /* Experiment.swift in Sources */, 6E75176122C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75180B22C520D400B2B157 /* DataStoreFile.swift in Sources */, + 6E6522E9278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6EF8DE2324BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7517C322C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190722C520D500B2B157 /* Attribute.swift in Sources */, 6E75179D22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7518BF22C520D400B2B157 /* Variable.swift in Sources */, + 6E6522D8278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75181722C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75185322C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E75173D22C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -4240,6 +4370,7 @@ 6E75187722C520D400B2B157 /* Variation.swift in Sources */, 6E7517F322C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E7518FB22C520D500B2B157 /* UserAttribute.swift in Sources */, + 6E65230B278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6E9B11B222C5489400C22D81 /* OTUtils.swift in Sources */, 6E7518D722C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516AD22C520D400B2B157 /* DefaultLogger.swift in Sources */, @@ -4262,6 +4393,7 @@ 6E7518CB22C520D400B2B157 /* Audience.swift in Sources */, 6ECB60D1234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, C78CAFAC24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, + 6E6522C5278DF27600954EA1 /* ZaiusApiManager.swift in Sources */, 6E75176D22C520D400B2B157 /* Utils.swift in Sources */, 6EA2CC2B2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E75182322C520D400B2B157 /* BatchEventBuilder.swift in Sources */, @@ -4300,6 +4432,7 @@ 6E9B117422C5487100C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B116E22C5487100C22D81 /* LoggerTests.swift in Sources */, 6E75180D22C520D400B2B157 /* DataStoreFile.swift in Sources */, + 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */, 6E75178722C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E75179F22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7516BB22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, @@ -4308,6 +4441,7 @@ 6E75184922C520D400B2B157 /* Event.swift in Sources */, 0B97DDA4249D4A27003DE606 /* SemanticVersion.swift in Sources */, 6ECB60D3234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, + 6E6522DA278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E9B116C22C5487100C22D81 /* BucketTests_ExpToVariation.swift in Sources */, 6E7E9C4C25240D31009E4426 /* OptimizelyUserContextTests_Objc.m in Sources */, 6E75193922C520D500B2B157 /* OPTDataStore.swift in Sources */, @@ -4339,6 +4473,7 @@ 6E7516EB22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75188522C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E75176F22C520D400B2B157 /* Utils.swift in Sources */, + 6E65230D278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6E75182522C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6EC6DD6A24AE94820017D296 /* OptimizelyUserContextTests.swift in Sources */, 6E7517D122C520D400B2B157 /* DefaultBucketer.swift in Sources */, @@ -4365,6 +4500,7 @@ 6E75183122C520D400B2B157 /* BatchEvent.swift in Sources */, 6EF41A422522BE2100EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */, 6E9B117822C5487100C22D81 /* DataStoreTests.swift in Sources */, + 6E6522FC278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E75171B22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75195122C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75176322C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -4386,6 +4522,7 @@ 6E9B116F22C5487100C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3F25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75174B22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, + 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75187922C520D400B2B157 /* Variation.swift in Sources */, 6E75191522C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75195D22C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -4424,6 +4561,7 @@ 6E75170422C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187A22C520D400B2B157 /* Variation.swift in Sources */, 0B97DD9A249D332C003DE606 /* SemanticVersionTests.swift in Sources */, + 6E65230E278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6E9B119C22C5488300C22D81 /* ProjectConfigTests.swift in Sources */, 6E7518FE22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7517F622C520D400B2B157 /* DataStoreMemory.swift in Sources */, @@ -4435,6 +4573,7 @@ 6E75190A22C520D500B2B157 /* Attribute.swift in Sources */, 6E75171022C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C822C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */, 6E75194622C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185622C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20051226B4D28600278087 /* MockLogger.swift in Sources */, @@ -4454,6 +4593,7 @@ 6E75183222C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7518DA22C520D400B2B157 /* AttributeValue.swift in Sources */, 6E9B119822C5488300C22D81 /* AudienceTests_Evaluate.swift in Sources */, + 6E6522DB278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75192222C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75177022C520D400B2B157 /* Utils.swift in Sources */, 6E7516E022C520D400B2B157 /* OPTUserProfileService.swift in Sources */, @@ -4480,6 +4620,7 @@ 6E7518E622C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75179422C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11B722C5489600C22D81 /* MockUrlSession.swift in Sources */, + 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E9B119222C5488300C22D81 /* FeatureVariableTests.swift in Sources */, 6E75176422C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF632445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4488,6 +4629,7 @@ 6E4544B8270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E7517A022C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517AC22C520D400B2B157 /* Array+Extension.swift in Sources */, + 6E6522FD278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6EA425A52218E6AE00B074B5 /* (null) in Sources */, 6E8A3D522637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E75180E22C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -4560,6 +4702,7 @@ 6E7516D922C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E7516E522C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7E9B552523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */, + 6E652305278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6EC6DD3524ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E20050926B4D28500278087 /* MockLogger.swift in Sources */, 6E75175122C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4572,17 +4715,21 @@ 6E75181322C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD6924AE94820017D296 /* OptimizelyUserContextTests.swift in Sources */, 6E75171522C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, + 6E6522E3278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6EA2CC272345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFA724486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E75185B22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E7516B522C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7516A922C520D400B2B157 /* DefaultLogger.swift in Sources */, + 6E6522BF278DF27300954EA1 /* ZaiusApiManager.swift in Sources */, 6E9B114822C5486E00C22D81 /* OptimizelySwiftSDKiOSTests.swift in Sources */, 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, + 6E6522D2278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E623F06253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E424BE2263228E90081004A /* AtomicArray.swift in Sources */, 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, + 6E6522F4278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E9B114F22C5486E00C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E7518F722C520D500B2B157 /* UserAttribute.swift in Sources */, 6E75174522C520D400B2B157 /* HandlerRegistryService.swift in Sources */, @@ -4654,6 +4801,7 @@ 6E7516FF22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187522C520D400B2B157 /* Variation.swift in Sources */, 0B97DD99249D332C003DE606 /* SemanticVersionTests.swift in Sources */, + 6E652309278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6E9B118622C5488100C22D81 /* ProjectConfigTests.swift in Sources */, 6E7518F922C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7517F122C520D400B2B157 /* DataStoreMemory.swift in Sources */, @@ -4665,6 +4813,7 @@ 6E75190522C520D500B2B157 /* Attribute.swift in Sources */, 6E75170B22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C322C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 6E6522C3278DF27500954EA1 /* ZaiusApiManager.swift in Sources */, 6E75194122C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185122C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20050D26B4D28500278087 /* MockLogger.swift in Sources */, @@ -4684,6 +4833,7 @@ 6E75182D22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7518D522C520D400B2B157 /* AttributeValue.swift in Sources */, 6E9B118222C5488100C22D81 /* AudienceTests_Evaluate.swift in Sources */, + 6E6522D6278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75191D22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75176B22C520D400B2B157 /* Utils.swift in Sources */, 6E7516DB22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, @@ -4710,6 +4860,7 @@ 6E7518E122C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75178F22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11AD22C5489300C22D81 /* MockUrlSession.swift in Sources */, + 6E6522E7278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E9B117C22C5488100C22D81 /* FeatureVariableTests.swift in Sources */, 6E75175F22C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF5E2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4718,6 +4869,7 @@ 6E4544B3270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75179B22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517A722C520D400B2B157 /* Array+Extension.swift in Sources */, + 6E6522F8278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6EA425962218E6AD00B074B5 /* (null) in Sources */, 6E8A3D4D2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E75180922C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -4762,6 +4914,7 @@ 6E75181622C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75188E22C520D400B2B157 /* Project.swift in Sources */, 6E994B3C25A3E6EA00999262 /* DecisionResponse.swift in Sources */, + 6E6522E8278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75189A22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3924ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E75179C22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -4799,6 +4952,7 @@ 6E75180A22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516B822C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C222C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, + 6E6522C4278DF27600954EA1 /* ZaiusApiManager.swift in Sources */, 6E75191E22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75193622C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75170022C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -4817,17 +4971,20 @@ 6E75173C22C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E75170C22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7518D622C520D400B2B157 /* AttributeValue.swift in Sources */, + 6E6522F9278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E7518A622C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75186A22C520D400B2B157 /* Rollout.swift in Sources */, 6E75178422C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EF8DE1424BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75175422C520D400B2B157 /* LogMessage.swift in Sources */, 6E20050E26B4D28500278087 /* MockLogger.swift in Sources */, + 6E6522D7278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75194222C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E34A61D2319EBB800BAE302 /* Notifications.swift in Sources */, 6E7517A822C520D400B2B157 /* Array+Extension.swift in Sources */, 6E7518EE22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E75185222C520D400B2B157 /* ProjectConfig.swift in Sources */, + 6E65230A278E688B00954EA1 /* SegmentsCache.swift in Sources */, C78CAF5F2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E623F0A253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75179022C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, @@ -4850,6 +5007,7 @@ 6E75181B22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75189322C520D400B2B157 /* Project.swift in Sources */, 6E994B4125A3E6EA00999262 /* DecisionResponse.swift in Sources */, + 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75189F22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3E24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E7517A122C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -4887,6 +5045,7 @@ 6E75180F22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516BD22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C722C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, + 6E6522C9278DF27900954EA1 /* ZaiusApiManager.swift in Sources */, 6E75192322C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75193B22C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75170522C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -4905,17 +5064,20 @@ 6E75174122C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E75171122C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7518DB22C520D400B2B157 /* AttributeValue.swift in Sources */, + 6E6522FE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E7518AB22C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75186F22C520D400B2B157 /* Rollout.swift in Sources */, 6E75178922C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EF8DE1924BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75175922C520D400B2B157 /* LogMessage.swift in Sources */, 6E20051326B4D28600278087 /* MockLogger.swift in Sources */, + 6E6522DC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75194722C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E34A6222319EBB800BAE302 /* Notifications.swift in Sources */, 6E7517AD22C520D400B2B157 /* Array+Extension.swift in Sources */, 6E7518F322C520D500B2B157 /* ConditionHolder.swift in Sources */, 6E75185722C520D400B2B157 /* ProjectConfig.swift in Sources */, + 6E65230F278E688B00954EA1 /* SegmentsCache.swift in Sources */, C78CAF642445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E623F0F253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75179522C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, @@ -4944,6 +5106,7 @@ 6E7517D422C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E994B3425A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75193C22C520D500B2B157 /* OPTDecisionService.swift in Sources */, + 6E6522CD278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75176622C520D400B2B157 /* Utils.swift in Sources */, 6E75177E22C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7516BE22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, @@ -4953,6 +5116,7 @@ 6E75180422C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E86CEA224FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75195422C520D500B2B157 /* OPTBucketer.swift in Sources */, + 6E6522BA278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, 6E75171E22C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */, @@ -4985,13 +5149,16 @@ 6E75184C22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E75181022C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD4124ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, + 6E652300278E688B00954EA1 /* SegmentsCache.swift in Sources */, 6E7516EE22C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75183422C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75171222C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E7516D622C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E7516B222C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 6E6522EF278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E75192422C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */, + 6E6522DE278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75193022C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7517E022C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E7516FA22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -5046,9 +5213,11 @@ C78CAFA624486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7518D222C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516A822C520D400B2B157 /* DefaultLogger.swift in Sources */, + 6E6522E2278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E623F05253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5A2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E75194A22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, + 6E6522D1278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E86CEA724FDC846005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E5D121F2638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E7516CC22C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -5082,13 +5251,16 @@ 6ECB60CC234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E75192622C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7517A422C520D400B2B157 /* Array+Extension.swift in Sources */, + 6E6522BE278DF27200954EA1 /* ZaiusApiManager.swift in Sources */, 6EF8DE0F24BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75186622C520D400B2B157 /* Rollout.swift in Sources */, 6E994B3725A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75183622C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75193E22C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75178C22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, + 6E6522F3278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E4544AE270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 6E652304278E688B00954EA1 /* SegmentsCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5120,6 +5292,7 @@ 75C71A1325E454460084187E /* OPTEventDispatcher.swift in Sources */, 75C71A1425E454460084187E /* DefaultDatafileHandler.swift in Sources */, 6E424BE0263228E90081004A /* AtomicArray.swift in Sources */, + 6E652303278E688B00954EA1 /* SegmentsCache.swift in Sources */, 75C71A1525E454460084187E /* DecisionInfo.swift in Sources */, 75C71A1625E454460084187E /* DefaultBucketer.swift in Sources */, 75C71A1725E454460084187E /* DefaultNotificationCenter.swift in Sources */, @@ -5127,14 +5300,17 @@ 75C71A1925E454460084187E /* DecisionReasons.swift in Sources */, 75C71A1A25E454460084187E /* DecisionResponse.swift in Sources */, 75C71A1B25E454460084187E /* DataStoreMemory.swift in Sources */, + 6E6522F2278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 75C71A1C25E454460084187E /* DataStoreUserDefaults.swift in Sources */, 75C71A1D25E454460084187E /* DataStoreFile.swift in Sources */, 75C71A1E25E454460084187E /* DataStoreQueueStackImpl.swift in Sources */, 75C71A1F25E454460084187E /* BatchEventBuilder.swift in Sources */, 75C71A2025E454460084187E /* BatchEvent.swift in Sources */, + 6E6522D0278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 75C71A2125E454460084187E /* EventForDispatch.swift in Sources */, 75C71A2225E454460084187E /* SemanticVersion.swift in Sources */, 75C71A2325E454460084187E /* Audience.swift in Sources */, + 6E6522E1278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 75C71A2425E454460084187E /* AttributeValue.swift in Sources */, 75C71A2525E454460084187E /* ConditionLeaf.swift in Sources */, 75C71A2625E454460084187E /* ConditionHolder.swift in Sources */, @@ -5151,6 +5327,7 @@ 75C71A3025E454460084187E /* FeatureFlag.swift in Sources */, 75C71A3125E454460084187E /* Group.swift in Sources */, 75C71A3225E454460084187E /* Variable.swift in Sources */, + 6E6522BD278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, 75C71A3325E454460084187E /* Attribute.swift in Sources */, 75C71A3425E454460084187E /* BackgroundingCallbacks.swift in Sources */, 75C71A3525E454460084187E /* OPTNotificationCenter.swift in Sources */, @@ -5194,6 +5371,7 @@ BD6485482491474500F30986 /* DefaultNotificationCenter.swift in Sources */, 6E994B3625A3E6EA00999262 /* DecisionResponse.swift in Sources */, BD6485492491474500F30986 /* OPTDecisionService.swift in Sources */, + 6E6522CF278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, BD64854A2491474500F30986 /* Utils.swift in Sources */, BD64854B2491474500F30986 /* ArrayEventForDispatch+Extension.swift in Sources */, BD64854C2491474500F30986 /* DefaultEventDispatcher.swift in Sources */, @@ -5203,6 +5381,7 @@ BD64854F2491474500F30986 /* DataStoreFile.swift in Sources */, 6E86CEA424FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, BD6485502491474500F30986 /* OPTBucketer.swift in Sources */, + 6E6522BC278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, BD6485512491474500F30986 /* OptimizelyResult.swift in Sources */, BD6485522491474500F30986 /* Constants.swift in Sources */, BD6485532491474500F30986 /* DefaultLogger.swift in Sources */, @@ -5235,13 +5414,16 @@ BD64856B2491474500F30986 /* ProjectConfig.swift in Sources */, BD64856C2491474500F30986 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD4324ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, + 6E652302278E688B00954EA1 /* SegmentsCache.swift in Sources */, BD64856D2491474500F30986 /* OptimizelyError.swift in Sources */, BD64856E2491474500F30986 /* EventForDispatch.swift in Sources */, BD64856F2491474500F30986 /* OptimizelyClient+ObjC.swift in Sources */, BD6485702491474500F30986 /* OPTUserProfileService.swift in Sources */, BD6485712491474500F30986 /* DefaultUserProfileService.swift in Sources */, + 6E6522F1278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, BD6485722491474500F30986 /* DataStoreQueueStack.swift in Sources */, BD6485732491474500F30986 /* OptimizelyConfig.swift in Sources */, + 6E6522E0278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, BD6485742491474500F30986 /* OPTDataStore.swift in Sources */, BD6485752491474500F30986 /* DefaultDecisionService.swift in Sources */, BD6485762491474500F30986 /* OptimizelyLogLevel.swift in Sources */, diff --git a/Sources/Audience Targeting/DefaultAudienceSegmentsHandler.swift b/Sources/Audience Targeting/DefaultAudienceSegmentsHandler.swift new file mode 100644 index 00000000..57c670af --- /dev/null +++ b/Sources/Audience Targeting/DefaultAudienceSegmentsHandler.swift @@ -0,0 +1,36 @@ +// +// 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 Foundation + +class DefaultAudienceSegmentsHandler: OPTAudienceSegmentsHandler { + + let testApiKey = "W4WzcEs-ABgXorzY7h1LCQ" + let testUserId = "d66a9d81923d4d2f99d8f64338976322" + + let zaiusMgr = ZaiusApiManager() + let cache = SegmentsCache() + + func fetchQualifiedSegments(apiKey: String, user: OptimizelyUserContext, completionHandler: @escaping ([String]?, Error?) -> Void) { + let apiKey = testApiKey + let userId = testUserId + + zaiusMgr.fetch(apiKey: apiKey, userId: userId) { segments, err in + completionHandler(segments, err) + } + } + +} diff --git a/Sources/Audience Targeting/OPTAudienceSegmentsHandler.swift b/Sources/Audience Targeting/OPTAudienceSegmentsHandler.swift new file mode 100644 index 00000000..1bf2bd2c --- /dev/null +++ b/Sources/Audience Targeting/OPTAudienceSegmentsHandler.swift @@ -0,0 +1,26 @@ +// +// 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 protocol OPTAudienceSegmentsHandler { + + func fetchQualifiedSegments(apiKey: String, + user: OptimizelyUserContext, + completionHandler: @escaping ([String]?, Error?) -> Void) + +} diff --git a/Sources/Audience Targeting/OptimizelyUserContext+ODP.swift b/Sources/Audience Targeting/OptimizelyUserContext+ODP.swift new file mode 100644 index 00000000..5a512177 --- /dev/null +++ b/Sources/Audience Targeting/OptimizelyUserContext+ODP.swift @@ -0,0 +1,46 @@ +// +// Copyright 2021-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 + +extension OptimizelyUserContext { + + // fetch (or read from cache) all qualified segments for the user attribute (userId default) + public func fetchQualifiedSegments(apiKey: String, completionHandler: @escaping (Bool) -> Void) { + optimizely?.audienceHandler.fetchQualifiedSegments(apiKey: apiKey, user: self) { segments, err in + if let err = err { + self.logger.e("Fetch segments failed with error: \(err)") + completionHandler(false) + return + } + + guard let segments = segments else { + self.logger.e("Fetch segments failed with invalid segments") + completionHandler(false) + return + } + + self.qualifiedSegments = Set(segments) + completionHandler(true) + } + } + + // true if the user is qualified for the given segment name + public func isQualifiedFor(segment: String) -> Bool { + return qualifiedSegments != nil + } + +} diff --git a/Sources/Audience Targeting/SegmentsCache.swift b/Sources/Audience Targeting/SegmentsCache.swift new file mode 100644 index 00000000..7c3039b2 --- /dev/null +++ b/Sources/Audience Targeting/SegmentsCache.swift @@ -0,0 +1,30 @@ +// +// 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 Foundation + +class SegmentsCache { + var map = [String: [String]]() + var order = [String]() + + func lookup(userId: String) -> [String]? { + return map[userId] + } + + func save(userId: String, segments: [String]) { + map[userId] = segments + } +} diff --git a/Sources/Audience Targeting/ZaiusApiManager.swift b/Sources/Audience Targeting/ZaiusApiManager.swift new file mode 100644 index 00000000..2b3cbf60 --- /dev/null +++ b/Sources/Audience Targeting/ZaiusApiManager.swift @@ -0,0 +1,158 @@ +// +// GraphQL.swift +// SwiftGraphQL +// +// Created by Jae Kim on 1/10/22. +// + +import Foundation + +// ODP GraphQL API +// - https://api.zaius.com/v3/graphql + +/* GraphQL Request + +query MyQuery { + customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { + audiences { + edges { + node { + name + is_ready + state + description + } + } + } + vuid + } +} +*/ + +/* GraphQL Response + { + "data": { + "customer": { + "audiences": { + "edges": [ + { + "node": { + "name": "has_email", + "is_ready": true, + "state": "qualified", + "description": "Customers who have an email address (regardless of consent/reachability status)" + } + }, + { + "node": { + "name": "has_email_opted_in", + "is_ready": true, + "state": "qualified", + "description": "Customers who have an email address, and it is opted-in" + } + }, + ... + ] + } + } + } + } + */ + +class ZaiusApiManager { + let apiHost = "https://api.zaius.com/v3/graphql" + + /* + curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name is_ready state description}}} vuid}}"}' https://api.zaius.com/v3/graphql + */ + + func fetch(apiKey: String, userId: String, completionHandler: @escaping ([String]?, Error?) -> Void) { + let body = [ + "query": "query {customer(vuid: \"\(userId)\") {audiences {edges {node {name is_ready state description}}}}}" + ] + guard let httpBody = try? JSONEncoder().encode(body) else { return } + + let url = URL(string: apiHost)! + 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 task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in + if let error = error { + print("[GraphQL Error] download failed: \(error)") + return + } + + if let data = data { + if let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { + if let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") { + let audiences = audDict.compactMap { ODPAudience($0["node"] as? [String: Any]) } + print("[GraphQL Response] \(audiences)") + + let segments = audiences.filter { $0.isQualified }.map { $0.name } + print("[GraphQL Audience Segments] \(segments)") + + completionHandler(segments, nil) + return + } + } else { + print("[GraphQL Error] decode failed: " + String(bytes: data, encoding: .utf8)!) + } + } else { + print("[GraphQL Error] data empty") + } + + completionHandler([], OptimizelyError.generic) + } + + task.resume() + } + +} + +struct ODPAudience: Decodable { + let name: String + let isReady: Bool + let state: String + let description: String + + var isQualified: Bool { + isReady && state == "qualified" + } + + init?(_ dict: [String: Any]?) { + guard let dict = dict, + let name = dict["name"] as? String, + let isReady = dict["is_ready"] as? Bool, + let state = dict["state"] as? String, + let description = dict["description"] as? String else { return nil } + + self.name = name + self.isReady = isReady + self.state = state + self.description = description + } +} + +// 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 as? T + } +} + diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 80a78599..843effbe 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -28,6 +28,9 @@ public class OptimizelyUserContext { var forcedDecisions: AtomicDictionary? + // ODP { get, set } + public var qualifiedSegments: Set? + var clone: OptimizelyUserContext? { guard let optimizely = self.optimizely else { return nil } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index a88fd9de..538aa95a 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -49,6 +49,9 @@ open class OptimizelyClient: NSObject { var eventDispatcher: OPTEventDispatcher? public var datafileHandler: OPTDatafileHandler? + // MARK: - ODP + public var audienceHandler: OPTAudienceSegmentsHandler = DefaultAudienceSegmentsHandler() + // MARK: - Default Services var decisionService: OPTDecisionService! From 6c01e73edbde78c9749534b78099c185fab16a8a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 12 Jan 2022 16:17:33 -0800 Subject: [PATCH 02/84] recover sample app --- DemoSwiftApp/AppDelegate.swift | 64 +++++++++++----------------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 3d5bdbc5..0a4c2823 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -102,11 +102,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func initializeOptimizelySDKWithCustomization() { // customization example (optional) + // You can enable background datafile polling by setting periodicDownloadInterval (polling is disabled by default) + // 60 sec interval may be too frequent. This is for demo purpose. (You can set this to nil to use the recommended value of 600 secs). + let downloadIntervalInSecs: Int? = 60 + + // You can turn off event batching with 0 timerInterval (this means that events are sent out immediately to the server instead of saving in the local queue for batching) + let eventDispatcher = DefaultEventDispatcher(timerInterval: 0) + + // customize logger + let customLogger = CustomLogger() optimizely = OptimizelyClient(sdkKey: sdkKey, + logger: customLogger, + eventDispatcher: eventDispatcher, + periodicDownloadInterval: downloadIntervalInSecs, defaultLogLevel: logLevel) - optimizely.start { result in + addNotificationListeners() + + // initialize SDK + optimizely!.start { result in switch result { case .failure(let error): print("Optimizely SDK initiliazation failed: \(error)") @@ -115,51 +130,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @unknown default: print("Optimizely SDK initiliazation failed with unknown result") } + self.startWithRootViewController() - let user = self.optimizely.createUserContext(userId: "tester") - user.fetchQualifiedSegments(apiKey: "valid-api-key") { success in - if success { - let decisions = user.decideAll(options: nil) - print("[ODP DECISIONS] \(decisions)") - } - } + // For sample codes for APIs, see "Samples/SamplesForAPI.swift" + //SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely) + //SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely) } - - -// // You can enable background datafile polling by setting periodicDownloadInterval (polling is disabled by default) -// // 60 sec interval may be too frequent. This is for demo purpose. (You can set this to nil to use the recommended value of 600 secs). -// let downloadIntervalInSecs: Int? = 60 -// -// // You can turn off event batching with 0 timerInterval (this means that events are sent out immediately to the server instead of saving in the local queue for batching) -// let eventDispatcher = DefaultEventDispatcher(timerInterval: 0) -// -// // customize logger -// let customLogger = CustomLogger() -// -// optimizely = OptimizelyClient(sdkKey: sdkKey, -// logger: customLogger, -// eventDispatcher: eventDispatcher, -// periodicDownloadInterval: downloadIntervalInSecs, -// defaultLogLevel: logLevel) -// -// addNotificationListeners() -// -// // initialize SDK -// optimizely!.start { result in -// switch result { -// case .failure(let error): -// print("Optimizely SDK initiliazation failed: \(error)") -// case .success: -// print("Optimizely SDK initialized successfully!") -// @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) -// } } func addNotificationListeners() { From a0ec55093982b9a2aec0f1a05bdba1d5817a3ff5 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 16 Mar 2022 14:41:36 -0700 Subject: [PATCH 03/84] add optional AudienceSegmentsHandler --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 6 +++--- .../DefaultAudienceSegmentsHandler.swift | 0 .../OPTAudienceSegmentsHandler.swift | 0 .../OptimizelyUserContext+ODP.swift | 8 +++++++- .../SegmentsCache.swift | 0 .../ZaiusApiManager.swift | 0 .../OptimizelyClient+Extension.swift | 6 ++++-- .../OptimizelyUserContext+ObjC.swift | 6 ++++-- .../OptimizelyUserContext.swift | 3 ++- Sources/Optimizely/OptimizelyClient.swift | 18 +++++++++++------- .../DecisionListenerTests.swift | 6 ++++-- .../OptimizelyUserContextTests_Objc.m | 3 ++- 12 files changed, 37 insertions(+), 19 deletions(-) rename Sources/{Audience Targeting => AudienceSegments}/DefaultAudienceSegmentsHandler.swift (100%) rename Sources/{Audience Targeting => AudienceSegments}/OPTAudienceSegmentsHandler.swift (100%) rename Sources/{Audience Targeting => AudienceSegments}/OptimizelyUserContext+ODP.swift (83%) rename Sources/{Audience Targeting => AudienceSegments}/SegmentsCache.swift (100%) rename Sources/{Audience Targeting => AudienceSegments}/ZaiusApiManager.swift (100%) diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 6738e06d..dcaa11be 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -2442,7 +2442,7 @@ path = "OptimizelyTests-MultiClients"; sourceTree = ""; }; - 6E6522B8278DF20F00954EA1 /* Audience Targeting */ = { + 6E6522B8278DF20F00954EA1 /* AudienceSegments */ = { isa = PBXGroup; children = ( 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */, @@ -2451,7 +2451,7 @@ 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */, 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */, ); - path = "Audience Targeting"; + path = AudienceSegments; sourceTree = ""; }; 6E6BE008237F547200FE8274 /* optimizelyConfig */ = { @@ -2466,11 +2466,11 @@ 6E75165D22C520D400B2B157 /* Sources */ = { isa = PBXGroup; children = ( - 6E6522B8278DF20F00954EA1 /* Audience Targeting */, 6E75166622C520D400B2B157 /* Optimizely */, 6EC6DD3F24ABF8180017D296 /* Optimizely+Decide */, 6E75165E22C520D400B2B157 /* Customization */, 6E75167C22C520D400B2B157 /* Implementation */, + 6E6522B8278DF20F00954EA1 /* AudienceSegments */, 6E75168822C520D400B2B157 /* Data Model */, 6E75169E22C520D400B2B157 /* Protocols */, 6E75167422C520D400B2B157 /* Extensions */, diff --git a/Sources/Audience Targeting/DefaultAudienceSegmentsHandler.swift b/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift similarity index 100% rename from Sources/Audience Targeting/DefaultAudienceSegmentsHandler.swift rename to Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift diff --git a/Sources/Audience Targeting/OPTAudienceSegmentsHandler.swift b/Sources/AudienceSegments/OPTAudienceSegmentsHandler.swift similarity index 100% rename from Sources/Audience Targeting/OPTAudienceSegmentsHandler.swift rename to Sources/AudienceSegments/OPTAudienceSegmentsHandler.swift diff --git a/Sources/Audience Targeting/OptimizelyUserContext+ODP.swift b/Sources/AudienceSegments/OptimizelyUserContext+ODP.swift similarity index 83% rename from Sources/Audience Targeting/OptimizelyUserContext+ODP.swift rename to Sources/AudienceSegments/OptimizelyUserContext+ODP.swift index 5a512177..60e7fa0e 100644 --- a/Sources/Audience Targeting/OptimizelyUserContext+ODP.swift +++ b/Sources/AudienceSegments/OptimizelyUserContext+ODP.swift @@ -20,7 +20,13 @@ extension OptimizelyUserContext { // fetch (or read from cache) all qualified segments for the user attribute (userId default) public func fetchQualifiedSegments(apiKey: String, completionHandler: @escaping (Bool) -> Void) { - optimizely?.audienceHandler.fetchQualifiedSegments(apiKey: apiKey, user: self) { segments, err in + guard let handler = optimizely?.audienceSegmentsHandler else { + self.logger.e("AudienceSegmentsHandler is not enabled") + completionHandler(false) + return + } + + handler.fetchQualifiedSegments(apiKey: apiKey, user: self) { segments, err in if let err = err { self.logger.e("Fetch segments failed with error: \(err)") completionHandler(false) diff --git a/Sources/Audience Targeting/SegmentsCache.swift b/Sources/AudienceSegments/SegmentsCache.swift similarity index 100% rename from Sources/Audience Targeting/SegmentsCache.swift rename to Sources/AudienceSegments/SegmentsCache.swift diff --git a/Sources/Audience Targeting/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift similarity index 100% rename from Sources/Audience Targeting/ZaiusApiManager.swift rename to Sources/AudienceSegments/ZaiusApiManager.swift diff --git a/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index e8be66c2..67021e46 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -63,7 +63,8 @@ extension OptimizelyClient { userProfileService: OPTUserProfileService? = nil, periodicDownloadInterval: Int?, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil) { + defaultDecideOptions: [OptimizelyDecideOption]? = nil, + enableAudienceSegmentsHandler: Bool = true) { self.init(sdkKey: sdkKey, logger: logger, @@ -71,7 +72,8 @@ extension OptimizelyClient { datafileHandler: datafileHandler, userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions) + defaultDecideOptions: defaultDecideOptions, + enableAudienceSegmentsHandler: enableAudienceSegmentsHandler) let interval = periodicDownloadInterval ?? 10 * 60 if interval > 0 { diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift b/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift index fa2a79b5..a639ceef 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift @@ -104,14 +104,16 @@ extension OptimizelyClient { userProfileService: OPTUserProfileService?, periodicDownloadInterval: NSNumber?, defaultLogLevel: OptimizelyLogLevel, - defaultDecideOptions: [Int]?) { + defaultDecideOptions: [Int]?, + enableAudienceSegmentsHandler: Bool) { self.init(sdkKey: sdkKey, logger: logger, eventDispatcher: SwiftEventDispatcher(eventDispatcher), userProfileService: userProfileService, periodicDownloadInterval: periodicDownloadInterval?.intValue, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: mapOptionsObjcToSwift(defaultDecideOptions)) + defaultDecideOptions: mapOptionsObjcToSwift(defaultDecideOptions), + enableAudienceSegmentsHandler: enableAudienceSegmentsHandler) } } diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 843effbe..c1186aa1 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -28,7 +28,8 @@ public class OptimizelyUserContext { var forcedDecisions: AtomicDictionary? - // ODP { get, set } + // MARK: - AudienceSegments + public var qualifiedSegments: Set? var clone: OptimizelyUserContext? { diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 538aa95a..2cb28570 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. @@ -48,15 +48,13 @@ open class OptimizelyClient: NSObject { var logger: OPTLogger! var eventDispatcher: OPTEventDispatcher? public var datafileHandler: OPTDatafileHandler? - - // MARK: - ODP - public var audienceHandler: OPTAudienceSegmentsHandler = DefaultAudienceSegmentsHandler() - + // MARK: - Default Services var decisionService: OPTDecisionService! + var audienceSegmentsHandler: OPTAudienceSegmentsHandler? public var notificationCenter: OPTNotificationCenter? - + // MARK: - Public interfaces /// OptimizelyClient init @@ -69,13 +67,15 @@ open class OptimizelyClient: NSObject { /// - userProfileService: custom UserProfileService (optional) /// - defaultLogLevel: default log level (optional. default = .info) /// - defaultDecisionOptions: default decision optiopns (optional) + /// - enableAudienceSegmentsHandler: enable AudienceSegmentsHandler (enabled by default). Set to false if not used. 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, + enableAudienceSegmentsHandler: Bool = true) { self.sdkKey = sdkKey self.defaultDecideOptions = defaultDecideOptions ?? [] @@ -86,6 +86,10 @@ open class OptimizelyClient: NSObject { let logger = logger ?? DefaultLogger() type(of: logger).logLevel = defaultLogLevel ?? .info + if enableAudienceSegmentsHandler { + self.audienceSegmentsHandler = DefaultAudienceSegmentsHandler() + } + self.registerServices(sdkKey: sdkKey, logger: logger, eventDispatcher: eventDispatcher ?? DefaultEventDispatcher.sharedInstance, diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index dc0b7a28..0b10b3f2 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, + enableAudienceSegmentsHandler: Bool = true) { // clear shared handlers HandlerRegistryService.shared.removeAll() @@ -1224,7 +1225,8 @@ class FakeManager: OptimizelyClient { datafileHandler: datafileHandler, userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions) + defaultDecideOptions: defaultDecideOptions, + enableAudienceSegmentsHandler: enableAudienceSegmentsHandler) let userProfileService = userProfileService ?? DefaultUserProfileService() self.decisionService = FakeDecisionService(userProfileService: userProfileService) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m index df4d840f..19c72bab 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m @@ -184,7 +184,8 @@ - (void)testDecide_defaultOptions { userProfileService:nil periodicDownloadInterval:0 defaultLogLevel:OptimizelyLogLevelDebug - defaultDecideOptions:defaultOptionsInObjcFormat]; + defaultDecideOptions:defaultOptionsInObjcFormat + enableAudienceSegmentsHandler:true]; [newOtimizely startWithDatafile:datafile error:nil]; user = [newOtimizely createUserContextWithUserId:kUserId attributes:@{@"gender": @"f"}]; From 1bd224a76c41a37b32cd4547715f2fa7d00a72c9 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 16 Mar 2022 17:37:46 -0700 Subject: [PATCH 04/84] clean up audience segments --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 172 +++++++----------- ...er.swift => AudienceSegmentsHandler.swift} | 22 ++- .../OptimizelyUserContext+ODP.swift | 52 ------ .../AudienceSegments/ZaiusApiManager.swift | 29 ++- .../OptimizelySegmentOption.swift} | 11 +- .../OptimizelyUserContext.swift | 69 ++++++- Sources/Optimizely/OptimizelyClient.swift | 4 +- 7 files changed, 176 insertions(+), 183 deletions(-) rename Sources/AudienceSegments/{DefaultAudienceSegmentsHandler.swift => AudienceSegmentsHandler.swift} (53%) delete mode 100644 Sources/AudienceSegments/OptimizelyUserContext+ODP.swift rename Sources/{AudienceSegments/OPTAudienceSegmentsHandler.swift => Optimizely+Decide/OptimizelySegmentOption.swift} (71%) diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index dcaa11be..dbf7c3b9 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -641,54 +641,22 @@ 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; 6E6522C9278DF27900954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522CD278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522CE278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522CF278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D0278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D1278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D2278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D3278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D4278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D5278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D6278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D7278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D8278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522D9278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522DA278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522DB278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522DC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */; }; - 6E6522DE278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522DF278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E0278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E1278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E2278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E3278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E4278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E5278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E6278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E7278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E8278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E9278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522EA278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522EF278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F0278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F1278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F2278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F3278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F4278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F5278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F6278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F7278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F8278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522F9278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522FA278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522FB278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522FC278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522FD278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; - 6E6522FE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */; }; + 6E6522DE278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522DF278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E0278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E2278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E3278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E4278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E5278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E6278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E7278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E8278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522EA278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522EB278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522EC278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522ED278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; 6E652300278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; 6E652301278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; 6E652302278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; @@ -1803,6 +1771,22 @@ 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 */; }; + 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 */; }; 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 */; }; @@ -2042,9 +2026,7 @@ 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 = ""; }; 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusApiManager.swift; sourceTree = ""; }; - 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPTAudienceSegmentsHandler.swift; sourceTree = ""; }; - 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAudienceSegmentsHandler.swift; sourceTree = ""; }; - 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OptimizelyUserContext+ODP.swift"; sourceTree = ""; }; + 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandler.swift; sourceTree = ""; }; 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentsCache.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 = ""; }; @@ -2233,6 +2215,7 @@ 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 = ""; }; + 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelySegmentOption.swift; sourceTree = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.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 = ""; }; @@ -2445,11 +2428,9 @@ 6E6522B8278DF20F00954EA1 /* AudienceSegments */ = { isa = PBXGroup; children = ( - 6E6522EE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift */, - 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */, + 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */, 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */, - 6E6522CC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift */, ); path = AudienceSegments; sourceTree = ""; @@ -2855,8 +2836,9 @@ children = ( 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */, 6EC6DD4024ABF89B0017D296 /* OptimizelyUserContext.swift */, - 6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */, 6EF8DE0A24BD1BB1008B9488 /* OptimizelyDecision.swift */, + 6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */, + 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, 6E86CEA124FDC836005DAFED /* OptimizelyUserContext+ObjC.swift */, ); path = "Optimizely+Decide"; @@ -3907,14 +3889,14 @@ C78CAFA824486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E14CD932423F9A700010234 /* Experiment.swift in Sources */, 6E14CD982423F9C300010234 /* BackgroundingCallbacks.swift in Sources */, - 6E6522E5278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E5278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E623F07253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5C2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E14CDA52423F9C300010234 /* MurmurHash3.swift in Sources */, - 6E6522D4278E4EA500954EA1 /* OPTAudienceSegmentsHandler.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 */, @@ -3952,7 +3934,6 @@ 6E14CDA82423F9C300010234 /* AtomicProperty.swift in Sources */, 6E14CD7A2423F98D00010234 /* OPTUserProfileService.swift in Sources */, 6E14CDA32423F9C300010234 /* Constants.swift in Sources */, - 6E6522F6278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E4544B1270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E652307278E688B00954EA1 /* SegmentsCache.swift in Sources */, ); @@ -3974,7 +3955,6 @@ 6E424CF326324B620081004A /* OPTDatafileHandler.swift in Sources */, 6E424CF426324B620081004A /* DecisionInfo.swift in Sources */, 6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */, - 6E6522D3278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */, 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */, @@ -3984,7 +3964,7 @@ 6E424CF926324B620081004A /* DecisionResponse.swift in Sources */, 6E424CFA26324B620081004A /* DataStoreMemory.swift in Sources */, 6E424CFB26324B620081004A /* DataStoreUserDefaults.swift in Sources */, - 6E6522E4278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E4278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */, 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */, 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */, @@ -4004,7 +3984,6 @@ 6E424D0926324B620081004A /* FeatureVariable.swift in Sources */, 6E6419DA2657059700C49555 /* NotificationCenterTests_MultiClients.swift in Sources */, 6E424D0A26324B620081004A /* Rollout.swift in Sources */, - 6E6522F5278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E2D5DAE26338CA00002077F /* AtomicDictionaryTests.swift in Sources */, 6E424D0B26324B620081004A /* Variation.swift in Sources */, 6E424D0C26324B620081004A /* TrafficAllocation.swift in Sources */, @@ -4029,6 +4008,7 @@ 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 /* SegmentsCache.swift in Sources */, @@ -4082,7 +4062,6 @@ 6E7518A122C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E994B3525A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E7516D722C520D400B2B157 /* OPTUserProfileService.swift in Sources */, - 6E6522CE278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75195522C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E75176722C520D400B2B157 /* Utils.swift in Sources */, 6E75180522C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -4131,10 +4110,10 @@ 6E75194922C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7516EF22C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75174F22C520D400B2B157 /* LogMessage.swift in Sources */, - 6E6522F0278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E75189522C520D400B2B157 /* Experiment.swift in Sources */, 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */, - 6E6522DF278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 84B4D75127E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 6E6522DF278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E7518D122C520D400B2B157 /* AttributeValue.swift in Sources */, 6E75182922C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75171F22C520D400B2B157 /* OptimizelyResult.swift in Sources */, @@ -4190,14 +4169,14 @@ C78CAFAD24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7516AE22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75195022C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 6E6522EA278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522EA278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E623F0C253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF612445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516D222C520D400B2B157 /* OPTLogger.swift in Sources */, - 6E6522D9278E4EA500954EA1 /* OPTAudienceSegmentsHandler.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 */, @@ -4235,7 +4214,6 @@ 6E75183C22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75194422C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75179222C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, - 6E6522FB278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E4544B6270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E65230C278E688B00954EA1 /* SegmentsCache.swift in Sources */, ); @@ -4287,7 +4265,6 @@ 6E623F08253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75188C22C520D400B2B157 /* Project.swift in Sources */, 6ECB60CE234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, - 6E6522F7278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E86CEA624FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75172222C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E7517E422C520D400B2B157 /* DefaultDecisionService.swift in Sources */, @@ -4304,6 +4281,7 @@ 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 */, @@ -4312,7 +4290,7 @@ 6E7517FC22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E75178222C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7518A422C520D400B2B157 /* FeatureFlag.swift in Sources */, - 6E6522E6278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E6278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75185C22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E20050C26B4D28500278087 /* MockLogger.swift in Sources */, 6E75176A22C520D400B2B157 /* Utils.swift in Sources */, @@ -4334,7 +4312,6 @@ 6E7518B022C520D400B2B157 /* Group.swift in Sources */, 6E75185022C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E7516CE22C520D400B2B157 /* OPTLogger.swift in Sources */, - 6E6522D5278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E9B11AB22C5489300C22D81 /* MockUrlSession.swift in Sources */, 6E75190422C520D500B2B157 /* Attribute.swift in Sources */, 6E75193422C520D500B2B157 /* OPTDataStore.swift in Sources */, @@ -4368,7 +4345,6 @@ 6E75191322C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 84E7ABC627D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E75172522C520D400B2B157 /* OptimizelyResult.swift in Sources */, - 6E6522FA278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E75173122C520D400B2B157 /* Constants.swift in Sources */, 6E75184722C520D400B2B157 /* Event.swift in Sources */, 6E994B3D25A3E6EA00999262 /* DecisionResponse.swift in Sources */, @@ -4379,13 +4355,12 @@ 6E75189B22C520D400B2B157 /* Experiment.swift in Sources */, 6E75176122C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75180B22C520D400B2B157 /* DataStoreFile.swift in Sources */, - 6E6522E9278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6EF8DE2324BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7517C322C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190722C520D500B2B157 /* Attribute.swift in Sources */, 6E75179D22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7518BF22C520D400B2B157 /* Variable.swift in Sources */, - 6E6522D8278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75181722C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75185322C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E75173D22C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -4401,6 +4376,7 @@ 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 */, @@ -4465,7 +4441,6 @@ 6E75184922C520D400B2B157 /* Event.swift in Sources */, 0B97DDA4249D4A27003DE606 /* SemanticVersion.swift in Sources */, 6ECB60D3234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, - 6E6522DA278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E9B116C22C5487100C22D81 /* BucketTests_ExpToVariation.swift in Sources */, 6E7E9C4C25240D31009E4426 /* OptimizelyUserContextTests_Objc.m in Sources */, 6E75193922C520D500B2B157 /* OPTDataStore.swift in Sources */, @@ -4525,7 +4500,6 @@ 6E75183122C520D400B2B157 /* BatchEvent.swift in Sources */, 6EF41A422522BE2100EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */, 6E9B117822C5487100C22D81 /* DataStoreTests.swift in Sources */, - 6E6522FC278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E75171B22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75195122C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75176322C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -4535,6 +4509,7 @@ 6E34A6202319EBB800BAE302 /* Notifications.swift in Sources */, 6E75173322C520D400B2B157 /* Constants.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 */, @@ -4547,7 +4522,7 @@ 6E9B116F22C5487100C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3F25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75174B22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522EB278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75187922C520D400B2B157 /* Variation.swift in Sources */, 6E75191522C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75195D22C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -4618,7 +4593,6 @@ 6E75183222C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7518DA22C520D400B2B157 /* AttributeValue.swift in Sources */, 6E9B119822C5488300C22D81 /* AudienceTests_Evaluate.swift in Sources */, - 6E6522DB278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75192222C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75177022C520D400B2B157 /* Utils.swift in Sources */, 6E7516E022C520D400B2B157 /* OPTUserProfileService.swift in Sources */, @@ -4646,7 +4620,7 @@ 6E7518E622C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75179422C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11B722C5489600C22D81 /* MockUrlSession.swift in Sources */, - 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522EC278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E9B119222C5488300C22D81 /* FeatureVariableTests.swift in Sources */, 6E75176422C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF632445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4655,7 +4629,6 @@ 6E4544B8270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E7517A022C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517AC22C520D400B2B157 /* Array+Extension.swift in Sources */, - 6E6522FD278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6EA425A52218E6AE00B074B5 /* (null) in Sources */, 6E8A3D522637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E75180E22C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -4678,6 +4651,7 @@ 6E7518CE22C520D400B2B157 /* Audience.swift in Sources */, 6E75189222C520D400B2B157 /* Project.swift in Sources */, 6E7516F822C520D400B2B157 /* OptimizelyError.swift in Sources */, + 84B4D75E27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E424C09263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75189E22C520D400B2B157 /* Experiment.swift in Sources */, 6E75178822C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4704,6 +4678,7 @@ 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 */, @@ -4741,7 +4716,7 @@ 6E75181322C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD6924AE94820017D296 /* OptimizelyUserContextTests.swift in Sources */, 6E75171522C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, - 6E6522E3278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E3278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6EA2CC272345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFA724486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E75185B22C520D400B2B157 /* FeatureVariable.swift in Sources */, @@ -4750,12 +4725,10 @@ 6E6522BF278DF27300954EA1 /* ZaiusApiManager.swift in Sources */, 6E9B114822C5486E00C22D81 /* OptimizelySwiftSDKiOSTests.swift in Sources */, 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, - 6E6522D2278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E623F06253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E424BE2263228E90081004A /* AtomicArray.swift in Sources */, 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, - 6E6522F4278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E9B114F22C5486E00C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E7518F722C520D500B2B157 /* UserAttribute.swift in Sources */, 6E75174522C520D400B2B157 /* HandlerRegistryService.swift in Sources */, @@ -4860,7 +4833,6 @@ 6E75182D22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7518D522C520D400B2B157 /* AttributeValue.swift in Sources */, 6E9B118222C5488100C22D81 /* AudienceTests_Evaluate.swift in Sources */, - 6E6522D6278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75191D22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75176B22C520D400B2B157 /* Utils.swift in Sources */, 6E7516DB22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, @@ -4888,7 +4860,7 @@ 6E7518E122C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75178F22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11AD22C5489300C22D81 /* MockUrlSession.swift in Sources */, - 6E6522E7278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E7278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E9B117C22C5488100C22D81 /* FeatureVariableTests.swift in Sources */, 6E75175F22C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF5E2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4897,7 +4869,6 @@ 6E4544B3270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75179B22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517A722C520D400B2B157 /* Array+Extension.swift in Sources */, - 6E6522F8278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6EA425962218E6AD00B074B5 /* (null) in Sources */, 6E8A3D4D2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E75180922C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -4920,6 +4891,7 @@ 6E7518C922C520D400B2B157 /* Audience.swift in Sources */, 6E75188D22C520D400B2B157 /* Project.swift in Sources */, 6E7516F322C520D400B2B157 /* OptimizelyError.swift in Sources */, + 84B4D75927E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E424C04263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75189922C520D400B2B157 /* Experiment.swift in Sources */, 6E75178322C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4942,7 +4914,7 @@ 6E75181622C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75188E22C520D400B2B157 /* Project.swift in Sources */, 6E994B3C25A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 6E6522E8278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E8278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75189A22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3924ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E75179C22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -4958,6 +4930,7 @@ 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 */, @@ -5000,14 +4973,12 @@ 6E75173C22C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E75170C22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7518D622C520D400B2B157 /* AttributeValue.swift in Sources */, - 6E6522F9278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E7518A622C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75186A22C520D400B2B157 /* Rollout.swift in Sources */, 6E75178422C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EF8DE1424BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75175422C520D400B2B157 /* LogMessage.swift in Sources */, 6E20050E26B4D28500278087 /* MockLogger.swift in Sources */, - 6E6522D7278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75194222C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E34A61D2319EBB800BAE302 /* Notifications.swift in Sources */, 6E7517A822C520D400B2B157 /* Array+Extension.swift in Sources */, @@ -5036,7 +5007,7 @@ 6E75181B22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75189322C520D400B2B157 /* Project.swift in Sources */, 6E994B4125A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522ED278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75189F22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3E24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E7517A122C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -5052,6 +5023,7 @@ 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 */, @@ -5094,14 +5066,12 @@ 6E75174122C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E75171122C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7518DB22C520D400B2B157 /* AttributeValue.swift in Sources */, - 6E6522FE278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E7518AB22C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75186F22C520D400B2B157 /* Rollout.swift in Sources */, 6E75178922C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EF8DE1924BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75175922C520D400B2B157 /* LogMessage.swift in Sources */, 6E20051326B4D28600278087 /* MockLogger.swift in Sources */, - 6E6522DC278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75194722C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E34A6222319EBB800BAE302 /* Notifications.swift in Sources */, 6E7517AD22C520D400B2B157 /* Array+Extension.swift in Sources */, @@ -5137,7 +5107,6 @@ 6E7517D422C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E994B3425A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75193C22C520D500B2B157 /* OPTDecisionService.swift in Sources */, - 6E6522CD278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 6E75176622C520D400B2B157 /* Utils.swift in Sources */, 6E75177E22C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7516BE22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, @@ -5186,10 +5155,10 @@ 6E75171222C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E7516D622C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E7516B222C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, - 6E6522EF278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E75192422C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */, - 6E6522DE278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 84B4D75027E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 6E6522DE278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75193022C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7517E022C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E7516FA22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -5245,14 +5214,14 @@ C78CAFA624486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7518D222C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516A822C520D400B2B157 /* DefaultLogger.swift in Sources */, - 6E6522E2278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E2278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E623F05253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5A2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E75194A22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 6E6522D1278E4EA500954EA1 /* OPTAudienceSegmentsHandler.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 */, @@ -5290,7 +5259,6 @@ 6E75183622C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75193E22C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75178C22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, - 6E6522F3278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 6E4544AE270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E652304278E688B00954EA1 /* SegmentsCache.swift in Sources */, ); @@ -5310,6 +5278,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 */, @@ -5332,17 +5301,15 @@ 75C71A1925E454460084187E /* DecisionReasons.swift in Sources */, 75C71A1A25E454460084187E /* DecisionResponse.swift in Sources */, 75C71A1B25E454460084187E /* DataStoreMemory.swift in Sources */, - 6E6522F2278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, 75C71A1C25E454460084187E /* DataStoreUserDefaults.swift in Sources */, 75C71A1D25E454460084187E /* DataStoreFile.swift in Sources */, 75C71A1E25E454460084187E /* DataStoreQueueStackImpl.swift in Sources */, 75C71A1F25E454460084187E /* BatchEventBuilder.swift in Sources */, 75C71A2025E454460084187E /* BatchEvent.swift in Sources */, - 6E6522D0278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, 75C71A2125E454460084187E /* EventForDispatch.swift in Sources */, 75C71A2225E454460084187E /* SemanticVersion.swift in Sources */, 75C71A2325E454460084187E /* Audience.swift in Sources */, - 6E6522E1278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 75C71A2425E454460084187E /* AttributeValue.swift in Sources */, 75C71A2525E454460084187E /* ConditionLeaf.swift in Sources */, 75C71A2625E454460084187E /* ConditionHolder.swift in Sources */, @@ -5405,7 +5372,6 @@ BD6485482491474500F30986 /* DefaultNotificationCenter.swift in Sources */, 6E994B3625A3E6EA00999262 /* DecisionResponse.swift in Sources */, BD6485492491474500F30986 /* OPTDecisionService.swift in Sources */, - 6E6522CF278E4EA500954EA1 /* OPTAudienceSegmentsHandler.swift in Sources */, BD64854A2491474500F30986 /* Utils.swift in Sources */, BD64854B2491474500F30986 /* ArrayEventForDispatch+Extension.swift in Sources */, BD64854C2491474500F30986 /* DefaultEventDispatcher.swift in Sources */, @@ -5454,10 +5420,10 @@ BD64856F2491474500F30986 /* OptimizelyClient+ObjC.swift in Sources */, BD6485702491474500F30986 /* OPTUserProfileService.swift in Sources */, BD6485712491474500F30986 /* DefaultUserProfileService.swift in Sources */, - 6E6522F1278E56D400954EA1 /* OptimizelyUserContext+ODP.swift in Sources */, BD6485722491474500F30986 /* DataStoreQueueStack.swift in Sources */, BD6485732491474500F30986 /* OptimizelyConfig.swift in Sources */, - 6E6522E0278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 84B4D75227E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 6E6522E0278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, BD6485742491474500F30986 /* OPTDataStore.swift in Sources */, BD6485752491474500F30986 /* DefaultDecisionService.swift in Sources */, BD6485762491474500F30986 /* OptimizelyLogLevel.swift in Sources */, diff --git a/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift b/Sources/AudienceSegments/AudienceSegmentsHandler.swift similarity index 53% rename from Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift rename to Sources/AudienceSegments/AudienceSegmentsHandler.swift index 57c670af..3945470b 100644 --- a/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/AudienceSegmentsHandler.swift @@ -15,20 +15,26 @@ // import Foundation +import UIKit -class DefaultAudienceSegmentsHandler: OPTAudienceSegmentsHandler { +class AudienceSegmentsHandler { - let testApiKey = "W4WzcEs-ABgXorzY7h1LCQ" - let testUserId = "d66a9d81923d4d2f99d8f64338976322" + static let reservedUserIdKey = "$opt_user_id" let zaiusMgr = ZaiusApiManager() let cache = SegmentsCache() + let logger = OPTLoggerFactory.getLogger() - func fetchQualifiedSegments(apiKey: String, user: OptimizelyUserContext, completionHandler: @escaping ([String]?, Error?) -> Void) { - let apiKey = testApiKey - let userId = testUserId - - zaiusMgr.fetch(apiKey: apiKey, userId: userId) { segments, err in + func fetchQualifiedSegments(apiKey: String, + userKey: String, + userValue: String, + segments: [String]? = nil, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, Error?) -> Void) { + zaiusMgr.fetch(apiKey: apiKey, + userKey: userKey, + userValue: userValue, + segments: segments) { segments, err in completionHandler(segments, err) } } diff --git a/Sources/AudienceSegments/OptimizelyUserContext+ODP.swift b/Sources/AudienceSegments/OptimizelyUserContext+ODP.swift deleted file mode 100644 index 60e7fa0e..00000000 --- a/Sources/AudienceSegments/OptimizelyUserContext+ODP.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// Copyright 2021-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 - -extension OptimizelyUserContext { - - // fetch (or read from cache) all qualified segments for the user attribute (userId default) - public func fetchQualifiedSegments(apiKey: String, completionHandler: @escaping (Bool) -> Void) { - guard let handler = optimizely?.audienceSegmentsHandler else { - self.logger.e("AudienceSegmentsHandler is not enabled") - completionHandler(false) - return - } - - handler.fetchQualifiedSegments(apiKey: apiKey, user: self) { segments, err in - if let err = err { - self.logger.e("Fetch segments failed with error: \(err)") - completionHandler(false) - return - } - - guard let segments = segments else { - self.logger.e("Fetch segments failed with invalid segments") - completionHandler(false) - return - } - - self.qualifiedSegments = Set(segments) - completionHandler(true) - } - } - - // true if the user is qualified for the given segment name - public func isQualifiedFor(segment: String) -> Bool { - return qualifiedSegments != nil - } - -} diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 2b3cbf60..6a544c5e 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -60,15 +60,30 @@ query MyQuery { */ class ZaiusApiManager { + let logger = OPTLoggerFactory.getLogger() + let apiHost = "https://api.zaius.com/v3/graphql" /* curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name is_ready state description}}} vuid}}"}' https://api.zaius.com/v3/graphql */ - func fetch(apiKey: String, userId: String, completionHandler: @escaping ([String]?, Error?) -> Void) { + func fetch(apiKey: String, + userKey: String, + userValue: String, + segments: [String]?, + completionHandler: @escaping ([String]?, Error?) -> Void) { + if userKey != "vuid" { + self.logger.e("Currently userKeys other than 'vuid' are not supported yet.") + return + } + + if (segments?.count ?? 0) > 0 { + self.logger.w("Selective segments fetching is not supported yet.") + } + let body = [ - "query": "query {customer(vuid: \"\(userId)\") {audiences {edges {node {name is_ready state description}}}}}" + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state description}}}}}" ] guard let httpBody = try? JSONEncoder().encode(body) else { return } @@ -82,7 +97,7 @@ class ZaiusApiManager { let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in if let error = error { - print("[GraphQL Error] download failed: \(error)") + self.logger.e("GraphQL download failed: \(error)") return } @@ -90,19 +105,19 @@ class ZaiusApiManager { if let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { if let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") { let audiences = audDict.compactMap { ODPAudience($0["node"] as? [String: Any]) } - print("[GraphQL Response] \(audiences)") + //print("[GraphQL Response] \(audiences)") let segments = audiences.filter { $0.isQualified }.map { $0.name } - print("[GraphQL Audience Segments] \(segments)") + //print("[GraphQL Audience Segments] \(segments)") completionHandler(segments, nil) return } } else { - print("[GraphQL Error] decode failed: " + String(bytes: data, encoding: .utf8)!) + self.logger.e("GraphQL decode failed: " + String(bytes: data, encoding: .utf8)!) } } else { - print("[GraphQL Error] data empty") + self.logger.e("GraphQL data empty") } completionHandler([], OptimizelyError.generic) diff --git a/Sources/AudienceSegments/OPTAudienceSegmentsHandler.swift b/Sources/Optimizely+Decide/OptimizelySegmentOption.swift similarity index 71% rename from Sources/AudienceSegments/OPTAudienceSegmentsHandler.swift rename to Sources/Optimizely+Decide/OptimizelySegmentOption.swift index 1bf2bd2c..926f71a9 100644 --- a/Sources/AudienceSegments/OPTAudienceSegmentsHandler.swift +++ b/Sources/Optimizely+Decide/OptimizelySegmentOption.swift @@ -16,11 +16,8 @@ import Foundation - -public protocol OPTAudienceSegmentsHandler { - - func fetchQualifiedSegments(apiKey: String, - user: OptimizelyUserContext, - completionHandler: @escaping ([String]?, Error?) -> Void) - +/// Options controlling audience segments. +@objc public enum OptimizelySegmentOption: Int { + // refresh the cache for the current target user + case forceCacheRefresh } diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index c1186aa1..0e9b4f36 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -27,10 +27,7 @@ public class OptimizelyUserContext { } var forcedDecisions: AtomicDictionary? - - // MARK: - AudienceSegments - - public var qualifiedSegments: Set? + public var qualifiedSegments: [String]? var clone: OptimizelyUserContext? { guard let optimizely = self.optimizely else { return nil } @@ -143,6 +140,70 @@ public class OptimizelyUserContext { } +// MARK: - AudienceSegments + +let testApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" +let testUserIdForAudienceSegments = "d66a9d81923d4d2f99d8f64338976322" + +extension OptimizelyUserContext { + + /// Fetch all qualified segments for the given user identifier (**userKey** and **userValue**). + /// + /// The **userId** of this context will be used by defaut when the user identifier is not given. + /// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time. + /// + /// - Parameters: + /// - apiKey: the public API key for the ODP account from which the audience segments will be fetched. + /// - userKey: The name of the user identifier (optional). + /// - userValue: The value of the user identifier (optional). + /// - options: A set of options for fetching qualified segments (optional). + /// - completionHandler: A completion handler to be called with the fetch status success/failure. + public func fetchQualifiedSegments(apiKey: String, + userKey: String? = nil, + userValue: String? = nil, + options: [OptimizelySegmentOption] = [], + completionHandler: @escaping (Bool) -> Void) { + guard let handler = optimizely?.audienceSegmentsHandler else { + self.logger.e("AudienceSegmentsHandler is not enabled") + completionHandler(false) + return + } + + //let segments = optimizely.getAllSegmentsForProject() + let segments = [String]() + + let userKey = userKey ?? AudienceSegmentsHandler.reservedUserIdKey + let userValue = userValue ?? userId + + handler.fetchQualifiedSegments(apiKey: apiKey, + userKey: userKey, + userValue: userValue, + segments: segments, + options: options) { segments, err in + if let err = err { + self.logger.e("Fetch segments failed with error: \(err)") + completionHandler(false) + return + } + + guard let segments = segments else { + self.logger.e("Fetch segments failed with invalid segments") + completionHandler(false) + return + } + + self.qualifiedSegments = segments + completionHandler(true) + } + } + + // true if the user is qualified for the given segment name + public func isQualifiedFor(segment: String) -> Bool { + return qualifiedSegments != nil + } + +} + // MARK: - ForcedDecisions /// Decision Context diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 2cb28570..7d57d343 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -52,7 +52,7 @@ open class OptimizelyClient: NSObject { // MARK: - Default Services var decisionService: OPTDecisionService! - var audienceSegmentsHandler: OPTAudienceSegmentsHandler? + var audienceSegmentsHandler: AudienceSegmentsHandler? public var notificationCenter: OPTNotificationCenter? // MARK: - Public interfaces @@ -87,7 +87,7 @@ open class OptimizelyClient: NSObject { type(of: logger).logLevel = defaultLogLevel ?? .info if enableAudienceSegmentsHandler { - self.audienceSegmentsHandler = DefaultAudienceSegmentsHandler() + self.audienceSegmentsHandler = AudienceSegmentsHandler() } self.registerServices(sdkKey: sdkKey, From f68e7d3ac641b50ef009b8e6cfb27b069d4dc761 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 22 Mar 2022 10:58:54 -0700 Subject: [PATCH 05/84] add config params --- Sources/AudienceSegments/AudienceSegmentsHandler.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/AudienceSegments/AudienceSegmentsHandler.swift b/Sources/AudienceSegments/AudienceSegmentsHandler.swift index 3945470b..9405a6c4 100644 --- a/Sources/AudienceSegments/AudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/AudienceSegmentsHandler.swift @@ -19,8 +19,12 @@ import UIKit class AudienceSegmentsHandler { - static let reservedUserIdKey = "$opt_user_id" + // configurable size + timeout + static var cacheMaxSize = 1000 + static var cacheTimeoutInSecs = 10*60 + static let reservedUserIdKey = "$opt_user_id" + let zaiusMgr = ZaiusApiManager() let cache = SegmentsCache() let logger = OPTLoggerFactory.getLogger() From ce958bcb1effa73046b5afbada7ce8fa6f4af633 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 5 Apr 2022 14:50:30 -0700 Subject: [PATCH 06/84] add tests for OptimizelyUserContext + ATS --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 110 ++++++++++++------ .../xcshareddata/IDETemplateMacros.plist | 2 +- ...t => DefaultAudienceSegmentsHandler.swift} | 8 +- .../AudienceSegments/ZaiusApiManager.swift | 9 +- .../OptimizelyClient+Extension.swift | 6 +- .../OptimizelyUserContext+ObjC.swift | 6 +- .../OptimizelyUserContext.swift | 35 +++--- Sources/Optimizely/OptimizelyClient.swift | 32 +++-- Sources/Optimizely/OptimizelyError.swift | 6 + .../DecisionListenerTests.swift | 6 +- .../OptimizelyUserContextTests_Objc.m | 3 +- .../OptimizelyUserContextTests_Segments.swift | 96 +++++++++++++++ 12 files changed, 232 insertions(+), 87 deletions(-) rename Sources/AudienceSegments/{AudienceSegmentsHandler.swift => DefaultAudienceSegmentsHandler.swift} (85%) create mode 100644 Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index dbf7c3b9..5ec9203a 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -641,22 +641,22 @@ 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; 6E6522C9278DF27900954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522DE278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522DF278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E0278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E2278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E3278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E4278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E5278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E6278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E7278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E8278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522EA278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522EB278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522EC278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522ED278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522DE278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522DF278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E0278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E1278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E2278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E3278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E4278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E5278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E6278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E7278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E8278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522E9278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522EA278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; 6E652300278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; 6E652301278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; 6E652302278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; @@ -1803,6 +1803,24 @@ 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_Segments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */; }; + 84F6BAB427FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */; }; + 84F6BAB627FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAB727FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAB827FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAB927FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BABA27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BABB27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BABC27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BABD27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BABE27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BABF27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAC027FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAC127FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAC227FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAC327FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAC427FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BAC527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.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 */; }; @@ -2026,7 +2044,7 @@ 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 = ""; }; 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusApiManager.swift; sourceTree = ""; }; - 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandler.swift; sourceTree = ""; }; + 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAudienceSegmentsHandler.swift; sourceTree = ""; }; 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentsCache.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 = ""; }; @@ -2217,6 +2235,8 @@ 75C71C3825E45A2B0084187E /* WatchBackgroundNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchBackgroundNotifier.swift; sourceTree = ""; }; 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelySegmentOption.swift; sourceTree = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; + 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; + 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPTAudienceSegmentHandler.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 = ""; }; @@ -2428,7 +2448,7 @@ 6E6522B8278DF20F00954EA1 /* AudienceSegments */ = { isa = PBXGroup; children = ( - 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */, + 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */, 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */, ); @@ -2626,6 +2646,7 @@ 6E7516A222C520D400B2B157 /* OPTDataStore.swift */, 6E7516A322C520D400B2B157 /* OPTDecisionService.swift */, 6E7516A522C520D400B2B157 /* OPTBucketer.swift */, + 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */, ); path = Protocols; sourceTree = ""; @@ -2729,8 +2750,9 @@ 6E27ECBD266FD78600B4A6D4 /* DecisionReasonsTests.swift */, 6E27EC9A266EF11000B4A6D4 /* OptimizelyDecisionTests.swift */, 6EC6DD6824AE94820017D296 /* OptimizelyUserContextTests.swift */, - 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */, 6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */, + 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */, + 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */, 6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */, 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */, 6E86CEB224FF20DE005DAFED /* OptimizelyUserContextTests_Objc.m */, @@ -3873,6 +3895,7 @@ 6E14CD942423F9A700010234 /* FeatureFlag.swift in Sources */, 6E14CD9D2423F9C300010234 /* OPTDatafileHandler.swift in Sources */, 6E424BE3263228E90081004A /* AtomicArray.swift in Sources */, + 84F6BABD27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3624BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E14CD7C2423F98D00010234 /* DefaultDatafileHandler.swift in Sources */, 6E14CD9B2423F9C300010234 /* OPTDataStore.swift in Sources */, @@ -3889,7 +3912,7 @@ C78CAFA824486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E14CD932423F9A700010234 /* Experiment.swift in Sources */, 6E14CD982423F9C300010234 /* BackgroundingCallbacks.swift in Sources */, - 6E6522E5278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E5278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E623F07253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5C2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E14CDA52423F9C300010234 /* MurmurHash3.swift in Sources */, @@ -3964,13 +3987,14 @@ 6E424CF926324B620081004A /* DecisionResponse.swift in Sources */, 6E424CFA26324B620081004A /* DataStoreMemory.swift in Sources */, 6E424CFB26324B620081004A /* DataStoreUserDefaults.swift in Sources */, - 6E6522E4278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E4278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */, 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */, 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */, 6E424CFF26324B620081004A /* BatchEvent.swift in Sources */, 6E424D0026324B620081004A /* EventForDispatch.swift in Sources */, 6E424D0126324B620081004A /* SemanticVersion.swift in Sources */, + 84F6BABC27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E424D0226324B620081004A /* Audience.swift in Sources */, 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */, @@ -4089,6 +4113,7 @@ 6E75172B22C520D400B2B157 /* Constants.swift in Sources */, 6E7516BF22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E424BDE263228E90081004A /* AtomicArray.swift in Sources */, + 84F6BAB727FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E7517E122C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75178B22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E75177F22C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4113,7 +4138,7 @@ 6E75189522C520D400B2B157 /* Experiment.swift in Sources */, 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84B4D75127E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522DF278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522DF278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E7518D122C520D400B2B157 /* AttributeValue.swift in Sources */, 6E75182922C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75171F22C520D400B2B157 /* OptimizelyResult.swift in Sources */, @@ -4153,6 +4178,7 @@ 6E7518C022C520D400B2B157 /* Variable.swift in Sources */, 6E75181822C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E424BE8263228E90081004A /* AtomicArray.swift in Sources */, + 84F6BAC227FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3B24BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75185422C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E9B11B422C5489500C22D81 /* OTUtils.swift in Sources */, @@ -4169,7 +4195,7 @@ C78CAFAD24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7516AE22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75195022C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 6E6522EA278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522EA278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E623F0C253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF612445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516D222C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4242,6 +4268,7 @@ 6E75174622C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E75181422C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */, + 84F6BABE27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E7516C222C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75188022C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E9B11DD22C548A200C22D81 /* OptimizelyClientTests_Valid.swift in Sources */, @@ -4290,7 +4317,7 @@ 6E7517FC22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E75178222C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7518A422C520D400B2B157 /* FeatureFlag.swift in Sources */, - 6E6522E6278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E6278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75185C22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E20050C26B4D28500278087 /* MockLogger.swift in Sources */, 6E75176A22C520D400B2B157 /* Utils.swift in Sources */, @@ -4353,9 +4380,10 @@ 0B97DDA2249D4A25003DE606 /* SemanticVersion.swift in Sources */, 6E7516C522C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75189B22C520D400B2B157 /* Experiment.swift in Sources */, + 84F6BAC127FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E75176122C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75180B22C520D400B2B157 /* DataStoreFile.swift in Sources */, - 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E9278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6EF8DE2324BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7517C322C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190722C520D500B2B157 /* Attribute.swift in Sources */, @@ -4444,6 +4472,7 @@ 6E9B116C22C5487100C22D81 /* BucketTests_ExpToVariation.swift in Sources */, 6E7E9C4C25240D31009E4426 /* OptimizelyUserContextTests_Objc.m in Sources */, 6E75193922C520D500B2B157 /* OPTDataStore.swift in Sources */, + 84F6BAC327FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E7518C122C520D400B2B157 /* Variable.swift in Sources */, 6E75170F22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6EC6DD4C24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, @@ -4457,6 +4486,7 @@ 6EF8DE2524BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75194522C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185522C520D400B2B157 /* ProjectConfig.swift in Sources */, + 84F6BAB427FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift in Sources */, 6E7516C722C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E9B11B522C5489600C22D81 /* MockUrlSession.swift in Sources */, 6EF8DE1724BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, @@ -4522,7 +4552,7 @@ 6E9B116F22C5487100C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3F25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75174B22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 6E6522EB278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75187922C520D400B2B157 /* Variation.swift in Sources */, 6E75191522C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75195D22C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -4587,6 +4617,7 @@ 6E86CEAE24FDC84A005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E9B119922C5488300C22D81 /* UserAttributeTests_Evaluate.swift in Sources */, 6E9B11A422C5488300C22D81 /* ProjectTests.swift in Sources */, + 84F6BAC427FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B119622C5488300C22D81 /* AudienceTests.swift in Sources */, 6E7518B622C520D400B2B157 /* Group.swift in Sources */, 6E7516D422C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4620,7 +4651,7 @@ 6E7518E622C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75179422C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11B722C5489600C22D81 /* MockUrlSession.swift in Sources */, - 6E6522EC278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E9B119222C5488300C22D81 /* FeatureVariableTests.swift in Sources */, 6E75176422C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF632445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4664,9 +4695,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 84F6BAB327FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift in Sources */, 6E7E9B372523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift in Sources */, 6E9B115622C5486E00C22D81 /* DecisionListenerTests.swift in Sources */, 6E9B114722C5486E00C22D81 /* OptimizelyErrorTests.swift in Sources */, + 84F6BABB27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B114B22C5486E00C22D81 /* BatchEventBuilderTest.swift in Sources */, 6E9B115A22C5486E00C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B115422C5486E00C22D81 /* LoggerTests.swift in Sources */, @@ -4716,7 +4749,7 @@ 6E75181322C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD6924AE94820017D296 /* OptimizelyUserContextTests.swift in Sources */, 6E75171522C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, - 6E6522E3278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E3278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6EA2CC272345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFA724486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E75185B22C520D400B2B157 /* FeatureVariable.swift in Sources */, @@ -4827,6 +4860,7 @@ 6E86CEA924FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E9B118322C5488100C22D81 /* UserAttributeTests_Evaluate.swift in Sources */, 6E9B118E22C5488100C22D81 /* ProjectTests.swift in Sources */, + 84F6BABF27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B118022C5488100C22D81 /* AudienceTests.swift in Sources */, 6E7518B122C520D400B2B157 /* Group.swift in Sources */, 6E7516CF22C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4860,7 +4894,7 @@ 6E7518E122C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75178F22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11AD22C5489300C22D81 /* MockUrlSession.swift in Sources */, - 6E6522E7278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E7278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E9B117C22C5488100C22D81 /* FeatureVariableTests.swift in Sources */, 6E75175F22C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF5E2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4914,7 +4948,7 @@ 6E75181622C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75188E22C520D400B2B157 /* Project.swift in Sources */, 6E994B3C25A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 6E6522E8278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E8278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75189A22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3924ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E75179C22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -4950,6 +4984,7 @@ 6E4544B4270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75184622C520D400B2B157 /* Event.swift in Sources */, 6E7517CE22C520D400B2B157 /* DefaultBucketer.swift in Sources */, + 84F6BAC027FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E75180A22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516B822C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C222C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, @@ -5007,7 +5042,7 @@ 6E75181B22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75189322C520D400B2B157 /* Project.swift in Sources */, 6E994B4125A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 6E6522ED278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75189F22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3E24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E7517A122C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -5043,6 +5078,7 @@ 6E4544B9270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75184B22C520D400B2B157 /* Event.swift in Sources */, 6E7517D322C520D400B2B157 /* DefaultBucketer.swift in Sources */, + 84F6BAC527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E75180F22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516BD22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C722C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, @@ -5134,6 +5170,7 @@ 6E75194822C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7518E822C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E424BDD263228E90081004A /* AtomicArray.swift in Sources */, + 84F6BAB627FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E75191822C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E7518B822C520D400B2B157 /* Variable.swift in Sources */, 6E75173622C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5158,7 +5195,7 @@ 6E75192422C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84B4D75027E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522DE278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522DE278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E75193022C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7517E022C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E7516FA22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -5198,6 +5235,7 @@ 6E7518BA22C520D400B2B157 /* Variable.swift in Sources */, 6E75181222C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E424BE1263228E90081004A /* AtomicArray.swift in Sources */, + 84F6BABA27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3424BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75184E22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E9B11A822C5489200C22D81 /* OTUtils.swift in Sources */, @@ -5214,7 +5252,7 @@ C78CAFA624486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7518D222C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516A822C520D400B2B157 /* DefaultLogger.swift in Sources */, - 6E6522E2278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E2278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 6E623F05253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5A2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E75194A22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -5287,6 +5325,7 @@ 75C71A0D25E454460084187E /* OptimizelyUserContext+ObjC.swift in Sources */, 75C71A0E25E454460084187E /* DefaultLogger.swift in Sources */, 75C71A0F25E454460084187E /* DefaultUserProfileService.swift in Sources */, + 84F6BAB927FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 75C71A1025E454460084187E /* DefaultEventDispatcher.swift in Sources */, 75C71A1125E454460084187E /* OPTLogger.swift in Sources */, 75C71A1225E454460084187E /* OPTUserProfileService.swift in Sources */, @@ -5309,7 +5348,7 @@ 75C71A2125E454460084187E /* EventForDispatch.swift in Sources */, 75C71A2225E454460084187E /* SemanticVersion.swift in Sources */, 75C71A2325E454460084187E /* Audience.swift in Sources */, - 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E1278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, 75C71A2425E454460084187E /* AttributeValue.swift in Sources */, 75C71A2525E454460084187E /* ConditionLeaf.swift in Sources */, 75C71A2625E454460084187E /* ConditionHolder.swift in Sources */, @@ -5399,6 +5438,7 @@ BD64855E2491474500F30986 /* OPTDatafileHandler.swift in Sources */, BD64855F2491474500F30986 /* ConditionHolder.swift in Sources */, 6E424BDF263228E90081004A /* AtomicArray.swift in Sources */, + 84F6BAB827FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, BD6485602491474500F30986 /* OPTNotificationCenter.swift in Sources */, BD6485612491474500F30986 /* Variable.swift in Sources */, BD6485622491474500F30986 /* MurmurHash3.swift in Sources */, @@ -5423,7 +5463,7 @@ BD6485722491474500F30986 /* DataStoreQueueStack.swift in Sources */, BD6485732491474500F30986 /* OptimizelyConfig.swift in Sources */, 84B4D75227E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522E0278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E0278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, BD6485742491474500F30986 /* OPTDataStore.swift in Sources */, BD6485752491474500F30986 /* DefaultDecisionService.swift in Sources */, BD6485762491474500F30986 /* OptimizelyLogLevel.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/AudienceSegments/AudienceSegmentsHandler.swift b/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift similarity index 85% rename from Sources/AudienceSegments/AudienceSegmentsHandler.swift rename to Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift index 9405a6c4..8e2324ee 100644 --- a/Sources/AudienceSegments/AudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift @@ -17,7 +17,7 @@ import Foundation import UIKit -class AudienceSegmentsHandler { +class DefaultAudienceSegmentsHandler: OPTAudienceSegmentsHandler { // configurable size + timeout static var cacheMaxSize = 1000 @@ -32,13 +32,13 @@ class AudienceSegmentsHandler { func fetchQualifiedSegments(apiKey: String, userKey: String, userValue: String, - segments: [String]? = nil, + segmentsToCheck: [String]? = nil, options: [OptimizelySegmentOption], - completionHandler: @escaping ([String]?, Error?) -> Void) { + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { zaiusMgr.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, - segments: segments) { segments, err in + segmentsToCheck: segmentsToCheck) { segments, err in completionHandler(segments, err) } } diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 6a544c5e..5c974034 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -71,14 +71,14 @@ class ZaiusApiManager { func fetch(apiKey: String, userKey: String, userValue: String, - segments: [String]?, - completionHandler: @escaping ([String]?, Error?) -> Void) { + segmentsToCheck: [String]?, + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { if userKey != "vuid" { self.logger.e("Currently userKeys other than 'vuid' are not supported yet.") return } - if (segments?.count ?? 0) > 0 { + if (segmentsToCheck?.count ?? 0) > 0 { self.logger.w("Selective segments fetching is not supported yet.") } @@ -156,6 +156,7 @@ struct ODPAudience: Decodable { // { "a": { "b": { "c": "contents" } } } extension Dictionary { + func extractComponent(keyPath: String) -> T? { var current: Any? = self @@ -169,5 +170,5 @@ extension Dictionary { return current as? T } + } - diff --git a/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index 67021e46..e8be66c2 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -63,8 +63,7 @@ extension OptimizelyClient { userProfileService: OPTUserProfileService? = nil, periodicDownloadInterval: Int?, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil, - enableAudienceSegmentsHandler: Bool = true) { + defaultDecideOptions: [OptimizelyDecideOption]? = nil) { self.init(sdkKey: sdkKey, logger: logger, @@ -72,8 +71,7 @@ extension OptimizelyClient { datafileHandler: datafileHandler, userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions, - enableAudienceSegmentsHandler: enableAudienceSegmentsHandler) + defaultDecideOptions: defaultDecideOptions) let interval = periodicDownloadInterval ?? 10 * 60 if interval > 0 { diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift b/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift index a639ceef..fa2a79b5 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift @@ -104,16 +104,14 @@ extension OptimizelyClient { userProfileService: OPTUserProfileService?, periodicDownloadInterval: NSNumber?, defaultLogLevel: OptimizelyLogLevel, - defaultDecideOptions: [Int]?, - enableAudienceSegmentsHandler: Bool) { + defaultDecideOptions: [Int]?) { self.init(sdkKey: sdkKey, logger: logger, eventDispatcher: SwiftEventDispatcher(eventDispatcher), userProfileService: userProfileService, periodicDownloadInterval: periodicDownloadInterval?.intValue, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: mapOptionsObjcToSwift(defaultDecideOptions), - enableAudienceSegmentsHandler: enableAudienceSegmentsHandler) + defaultDecideOptions: mapOptionsObjcToSwift(defaultDecideOptions)) } } diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 0e9b4f36..b49f278f 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -149,7 +149,7 @@ extension OptimizelyUserContext { /// Fetch all qualified segments for the given user identifier (**userKey** and **userValue**). /// - /// The **userId** of this context will be used by defaut when the user identifier is not given. + /// The **userId** of this context will be used by default when the user identifier is not provided. /// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time. /// /// - Parameters: @@ -162,44 +162,37 @@ extension OptimizelyUserContext { userKey: String? = nil, userValue: String? = nil, options: [OptimizelySegmentOption] = [], - completionHandler: @escaping (Bool) -> Void) { - guard let handler = optimizely?.audienceSegmentsHandler else { - self.logger.e("AudienceSegmentsHandler is not enabled") - completionHandler(false) + completionHandler: @escaping (OptimizelyError?) -> Void) { + guard let optimizely = self.optimizely else { + completionHandler(.sdkNotReady) return } - - //let segments = optimizely.getAllSegmentsForProject() - let segments = [String]() - - let userKey = userKey ?? AudienceSegmentsHandler.reservedUserIdKey + + let userKey = userKey ?? DefaultAudienceSegmentsHandler.reservedUserIdKey let userValue = userValue ?? userId - handler.fetchQualifiedSegments(apiKey: apiKey, - userKey: userKey, - userValue: userValue, - segments: segments, - options: options) { segments, err in + optimizely.fetchQualifiedSegments(apiKey: apiKey, + userKey: userKey, + userValue: userValue, + options: options) { segments, err in if let err = err { - self.logger.e("Fetch segments failed with error: \(err)") - completionHandler(false) + completionHandler(err) return } guard let segments = segments else { - self.logger.e("Fetch segments failed with invalid segments") - completionHandler(false) + completionHandler(.fetchSegmentsFailed("invalid segments")) return } self.qualifiedSegments = segments - completionHandler(true) + completionHandler(nil) } } // true if the user is qualified for the given segment name public func isQualifiedFor(segment: String) -> Bool { - return qualifiedSegments != nil + return qualifiedSegments?.contains(segment) ?? false } } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 7d57d343..f745a82d 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -52,7 +52,7 @@ open class OptimizelyClient: NSObject { // MARK: - Default Services var decisionService: OPTDecisionService! - var audienceSegmentsHandler: AudienceSegmentsHandler? + var audienceSegmentsHandler: OPTAudienceSegmentsHandler? public var notificationCenter: OPTNotificationCenter? // MARK: - Public interfaces @@ -67,15 +67,13 @@ open class OptimizelyClient: NSObject { /// - userProfileService: custom UserProfileService (optional) /// - defaultLogLevel: default log level (optional. default = .info) /// - defaultDecisionOptions: default decision optiopns (optional) - /// - enableAudienceSegmentsHandler: enable AudienceSegmentsHandler (enabled by default). Set to false if not used. public init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, datafileHandler: OPTDatafileHandler? = nil, userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil, - enableAudienceSegmentsHandler: Bool = true) { + defaultDecideOptions: [OptimizelyDecideOption]? = nil) { self.sdkKey = sdkKey self.defaultDecideOptions = defaultDecideOptions ?? [] @@ -86,10 +84,6 @@ open class OptimizelyClient: NSObject { let logger = logger ?? DefaultLogger() type(of: logger).logLevel = defaultLogLevel ?? .info - if enableAudienceSegmentsHandler { - self.audienceSegmentsHandler = AudienceSegmentsHandler() - } - self.registerServices(sdkKey: sdkKey, logger: logger, eventDispatcher: eventDispatcher ?? DefaultEventDispatcher.sharedInstance, @@ -754,6 +748,28 @@ open class OptimizelyClient: NSObject { return OptimizelyConfigImp(projectConfig: config) } + + // MARK: - AudienceSegmentsHandler + + func fetchQualifiedSegments(apiKey: String, + userKey: String, + userValue: String, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + // instantiate on the first request + let handler = audienceSegmentsHandler ?? DefaultAudienceSegmentsHandler() + + //let segmentsToCheck = getAllSegmentsForProject() + let segmentsToCheck = [String]() + + handler.fetchQualifiedSegments(apiKey: apiKey, + userKey: userKey, + userValue: userValue, + segmentsToCheck: segmentsToCheck, + options: options, + completionHandler: completionHandler) + } + } // MARK: - Send Events diff --git a/Sources/Optimizely/OptimizelyError.swift b/Sources/Optimizely/OptimizelyError.swift index 2d0766ab..060c7de6 100644 --- a/Sources/Optimizely/OptimizelyError.swift +++ b/Sources/Optimizely/OptimizelyError.swift @@ -79,6 +79,10 @@ public enum OptimizelyError: Error { case eventDispatchFailed(_ reason: String) case eventDispatcherConfigError(_ reason: String) + + // MARK: - AudienceSegements Errors + + case fetchSegmentsFailed(_ reason: String) } // MARK: - CustomStringConvertible @@ -147,6 +151,8 @@ extension OptimizelyError: CustomStringConvertible, ReasonProtocol { case .eventDispatchFailed(let hint): message = "Event dispatch failed (\(hint))." case .eventDispatcherConfigError(let hint): message = "EventDispatcher config error (\(hint))." + + case .fetchSegmentsFailed(let hint): message = "Fetch segments failed (\(hint))." } return message diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index 0b10b3f2..dc0b7a28 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift @@ -1213,8 +1213,7 @@ class FakeManager: OptimizelyClient { datafileHandler: OPTDatafileHandler? = nil, userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil, - enableAudienceSegmentsHandler: Bool = true) { + defaultDecideOptions: [OptimizelyDecideOption]? = nil) { // clear shared handlers HandlerRegistryService.shared.removeAll() @@ -1225,8 +1224,7 @@ class FakeManager: OptimizelyClient { datafileHandler: datafileHandler, userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions, - enableAudienceSegmentsHandler: enableAudienceSegmentsHandler) + defaultDecideOptions: defaultDecideOptions) let userProfileService = userProfileService ?? DefaultUserProfileService() self.decisionService = FakeDecisionService(userProfileService: userProfileService) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m index 19c72bab..df4d840f 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m @@ -184,8 +184,7 @@ - (void)testDecide_defaultOptions { userProfileService:nil periodicDownloadInterval:0 defaultLogLevel:OptimizelyLogLevelDebug - defaultDecideOptions:defaultOptionsInObjcFormat - enableAudienceSegmentsHandler:true]; + defaultDecideOptions:defaultOptionsInObjcFormat]; [newOtimizely startWithDatafile:datafile error:nil]; user = [newOtimizely createUserContextWithUserId:kUserId attributes:@{@"gender": @"f"}]; diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift new file mode 100644 index 00000000..cbdf5592 --- /dev/null +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -0,0 +1,96 @@ +// +// 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_Segments: XCTestCase { + + var optimizely: OptimizelyClient! + var user: OptimizelyUserContext! + let kUserId = "tester" + + override func setUp() { + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + optimizely.audienceSegmentsHandler = MockAudienceSegmentsHandler() + user = optimizely.createUserContext(userId: kUserId) + } + + 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")) + } + + func testFetchQualifiedSegments_successDefaultUser() { + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: MockAudienceSegmentsHandler.kApiKeyGood) { error in + XCTAssertNil(error) + XCTAssert(self.user.qualifiedSegments == [MockAudienceSegmentsHandler.kApiKeyGood, "$opt_user_id", self.kUserId]) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + + func testFetchQualifiedSegments_sdkNotReady() { + user.optimizely = nil + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: MockAudienceSegmentsHandler.kApiKeyGood) { error in + XCTAssertEqual(OptimizelyError.sdkNotReady.reason, error?.reason) + XCTAssertNil(self.user.qualifiedSegments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + + func testFetchQualifiedSegments_fetchFailed() { + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: MockAudienceSegmentsHandler.kApiKeyBad) { error in + XCTAssertNotNil(error) + XCTAssertNil(self.user.qualifiedSegments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + +} + +class MockAudienceSegmentsHandler: OPTAudienceSegmentsHandler { + static let kApiKeyGood = "apiKeyGood" + static let kApiKeyBad = "apiKeyBad" + + func fetchQualifiedSegments(apiKey: String, + userKey: String, + userValue: String, + segmentsToCheck: [String]?, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + if apiKey == MockAudienceSegmentsHandler.kApiKeyGood { + let sampleSegments = [apiKey, userKey, userValue] + completionHandler(sampleSegments, nil) + } else { + completionHandler(nil, OptimizelyError.generic) + } + } + } +} + From d5721b32b76881462c12dd71bf389fbd7e16acf2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 5 Apr 2022 14:51:01 -0700 Subject: [PATCH 07/84] add protocol for AudienceSegmentsHandler --- .../Protocols/OPTAudienceSegmentHandler.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Sources/Protocols/OPTAudienceSegmentHandler.swift diff --git a/Sources/Protocols/OPTAudienceSegmentHandler.swift b/Sources/Protocols/OPTAudienceSegmentHandler.swift new file mode 100644 index 00000000..6404fa9e --- /dev/null +++ b/Sources/Protocols/OPTAudienceSegmentHandler.swift @@ -0,0 +1,28 @@ +// +// 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. +// 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 + +protocol OPTAudienceSegmentsHandler { + + func fetchQualifiedSegments(apiKey: String, + userKey: String, + userValue: String, + segmentsToCheck: [String]?, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) + +} From 5fb8a202c03606b7786db70ef6d80798d6ac9fb3 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 5 Apr 2022 17:02:30 -0700 Subject: [PATCH 08/84] more tests for segments --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 20 +++ .../OptimizelyUserContextTests_Segments.swift | 2 + ...zelyUserContextTests_Segments_Decide.swift | 86 +++++++++++ .../decide/decide_audience_segments.json | 136 ++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift create mode 100644 Tests/TestData/decide/decide_audience_segments.json diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 5ec9203a..4e40ee67 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1821,6 +1821,14 @@ 84F6BAC327FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; 84F6BAC427FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; 84F6BAC527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; + 84F6BACC27FCFAD7004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; + 84F6BAD727FCFB14004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; + 84F6BAD827FCFB15004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; + 84F6BAD927FCFB17004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; + 84F6BADA27FCFB18004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; + 84F6BADB27FCFB21004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; + 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift */; }; + 84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_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 */; }; @@ -2237,6 +2245,8 @@ 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPTAudienceSegmentHandler.swift; sourceTree = ""; }; + 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_audience_segments.json; sourceTree = ""; }; + 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments_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 = ""; }; @@ -2752,6 +2762,7 @@ 6EC6DD6824AE94820017D296 /* OptimizelyUserContextTests.swift */, 6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */, 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */, + 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift */, 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */, 6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */, 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */, @@ -2870,6 +2881,7 @@ isa = PBXGroup; children = ( 6EF8DE0524B8DA58008B9488 /* decide_datafile.json */, + 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */, ); path = decide; sourceTree = ""; @@ -3383,6 +3395,7 @@ 6EE59299264DF4990013AD66 /* bucketer_test2.json in Resources */, 6EE592AC264DF4990013AD66 /* unsupported_datafile.json in Resources */, 6EE592A8264DF4990013AD66 /* optimizely_6372300739_v4.json in Resources */, + 84F6BADB27FCFB21004BE62A /* decide_audience_segments.json in Resources */, 6EE5929F264DF4990013AD66 /* rollout_bucketing.json in Resources */, 6EE592A0264DF4990013AD66 /* feature_management_experiment_bucketing.json in Resources */, 6EE59298264DF4990013AD66 /* bucketer_test.json in Resources */, @@ -3465,6 +3478,7 @@ 6E12B20622C55A270005E9E6 /* grouped_experiments.json in Resources */, 6E34A629231ED04900BAE302 /* empty_datafile_new_project_id.json in Resources */, 6E12B20322C55A270005E9E6 /* feature_variables.json in Resources */, + 84F6BAD727FCFB14004BE62A /* decide_audience_segments.json in Resources */, 6E12B20822C55A270005E9E6 /* audience_targeting.json in Resources */, 6E12B1FB22C55A270005E9E6 /* feature_rollout_toggle_on.json in Resources */, 6E12B1FC22C55A270005E9E6 /* feature_rollout_toggle_off.json in Resources */, @@ -3550,6 +3564,7 @@ 6E12B27522C55A290005E9E6 /* feature_experiments.json in Resources */, 6E12B28722C55A290005E9E6 /* bot_filtering_enabled.json in Resources */, 6E12B28422C55A290005E9E6 /* bucketer_test.json in Resources */, + 84F6BAD927FCFB17004BE62A /* decide_audience_segments.json in Resources */, 6E12B28322C55A290005E9E6 /* empty_datafile.json in Resources */, 6E12B28522C55A290005E9E6 /* bucketer_test2.json in Resources */, 6E12B28222C55A290005E9E6 /* feature_exp.json in Resources */, @@ -3590,6 +3605,7 @@ 6E12B28D22C55A2A0005E9E6 /* feature_experiments.json in Resources */, 6E12B29F22C55A2A0005E9E6 /* bot_filtering_enabled.json in Resources */, 6E12B29C22C55A2A0005E9E6 /* bucketer_test.json in Resources */, + 84F6BADA27FCFB18004BE62A /* decide_audience_segments.json in Resources */, 6E12B29B22C55A2A0005E9E6 /* empty_datafile.json in Resources */, 6E12B29D22C55A2A0005E9E6 /* bucketer_test2.json in Resources */, 6E12B29A22C55A2A0005E9E6 /* feature_exp.json in Resources */, @@ -3630,6 +3646,7 @@ 6E12B1E522C55A260005E9E6 /* feature_experiments.json in Resources */, 6E12B1F722C55A260005E9E6 /* bot_filtering_enabled.json in Resources */, 6E12B1F422C55A260005E9E6 /* bucketer_test.json in Resources */, + 84F6BACC27FCFAD7004BE62A /* decide_audience_segments.json in Resources */, 6E12B1F322C55A260005E9E6 /* empty_datafile.json in Resources */, 6E12B1F522C55A260005E9E6 /* bucketer_test2.json in Resources */, 6E12B1F222C55A260005E9E6 /* feature_exp.json in Resources */, @@ -3670,6 +3687,7 @@ 6E12B21522C55A270005E9E6 /* feature_experiments.json in Resources */, 6E12B22722C55A270005E9E6 /* bot_filtering_enabled.json in Resources */, 6E12B22422C55A270005E9E6 /* bucketer_test.json in Resources */, + 84F6BAD827FCFB15004BE62A /* decide_audience_segments.json in Resources */, 6E12B22322C55A270005E9E6 /* empty_datafile.json in Resources */, 6E12B22522C55A270005E9E6 /* bucketer_test2.json in Resources */, 6E12B22222C55A270005E9E6 /* feature_exp.json in Resources */, @@ -4459,6 +4477,7 @@ 6E9B116522C5487100C22D81 /* BatchEventBuilderTest.swift in Sources */, 6E9B117422C5487100C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B116E22C5487100C22D81 /* LoggerTests.swift in Sources */, + 84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */, 6E75180D22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */, 6E75178722C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4759,6 +4778,7 @@ 6E9B114822C5486E00C22D81 /* OptimizelySwiftSDKiOSTests.swift in Sources */, 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, + 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */, 6E623F06253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E424BE2263228E90081004A /* AtomicArray.swift in Sources */, 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index cbdf5592..0bb5c574 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -73,6 +73,8 @@ class OptimizelyUserContextTests_Segments: XCTestCase { } +// MARK: - MockAudienceSegmentsHandler + class MockAudienceSegmentsHandler: OPTAudienceSegmentsHandler { static let kApiKeyGood = "apiKeyGood" static let kApiKeyBad = "apiKeyBad" diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift new file mode 100644 index 00000000..837c1e09 --- /dev/null +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_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_Segments_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) + 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/TestData/decide/decide_audience_segments.json b/Tests/TestData/decide/decide_audience_segments.json new file mode 100644 index 00000000..eb289768 --- /dev/null +++ b/Tests/TestData/decide/decide_audience_segments.json @@ -0,0 +1,136 @@ +{ + "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", "13389141123", "13389142234"], + "variations": [ + { + "variables": [], + "featureEnabled": true, + "id": "10389729780", + "key": "variation-a" + }, + { + "variables": [], + "id": "10416523121", + "key": "variation-b" + } + ], + "forcedVariations": {}, + "id": "10390977673" + } + ], + "groups": [], + "audiences": [ + { + "id": "13389141123", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"custom_attribute\", \"value\": 20}]]]", + "name": "adult" + }, + { + "id": "13389142234", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"qualified\", \"name\": \"$opt_user_id\", \"type\": \"segment\", \"value\": \"odp-segement-1\"}]]]", + "name": "odp-segment" + }, + { + "id": "13389130056", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"qualified\", \"name\": \"$opt_user_id\", \"type\": \"segment\", \"value\": \"odp-segement-2\"}]]]", + "name": "odp-segment" + } + ], + "attributes": [ + { + "id": "10401066117", + "key": "gender" + }, + { + "id": "10401066170", + "key": "testvar" + } + ], + "accountId": "10367498574", + "events": [], + "revision": "101" +} From 619d524c6044dcab891f53b410afac5d42b75090 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 6 Apr 2022 16:49:14 -0700 Subject: [PATCH 09/84] add segments to audience --- Sources/Data Model/Audience/Audience.swift | 4 +- .../Data Model/Audience/ConditionHolder.swift | 16 +- .../Data Model/Audience/ConditionLeaf.swift | 6 +- .../Data Model/Audience/UserAttribute.swift | 64 ++-- Sources/Data Model/Project.swift | 6 +- .../DefaultDecisionService.swift | 15 +- .../OptimizelyUserContext.swift | 23 +- Sources/Utils/AtomicArray.swift | 8 +- .../OptimizelyClientTests.swift | 3 +- .../DecisionServiceTests_Experiments.swift | 82 ++--- .../DecisionServiceTests_Others.swift | 3 +- .../OptimizelySwiftSDKiOSTests.swift | 2 +- ...zelyUserContextTests_Segments_Decide.swift | 2 +- .../AudienceTests_Evaluate.swift | 286 +++++++++--------- .../ConditionHolderTests.swift | 4 +- .../ConditionHolderTests_Evaluate.swift | 42 +-- .../ConditionLeafTests.swift | 2 +- .../UserAttributeTests.swift | 4 +- .../UserAttributeTests_Evaluate.swift | 144 ++++----- .../decide/decide_audience_segments.json | 4 +- Tests/TestUtils/OTUtils.swift | 8 + 21 files changed, 370 insertions(+), 358 deletions(-) diff --git a/Sources/Data Model/Audience/Audience.swift b/Sources/Data Model/Audience/Audience.swift index aafa5f92..e502423c 100644 --- a/Sources/Data Model/Audience/Audience.swift +++ b/Sources/Data Model/Audience/Audience.swift @@ -70,8 +70,8 @@ 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) } } 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..cdfc8b66 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,73 @@ extension UserAttribute { throw OptimizelyError.userAttributeInvalidName(stringRepresentation) } - let attributes = attributes ?? OptimizelyAttributes() - + let attributes = user.attributes let rawAttributeValue = 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 value == nil { - throw OptimizelyError.userAttributeNilValue(stringRepresentation) - } + if matchFinal == .exists { + return !(rawAttributeValue is NSNull || rawAttributeValue == nil) + } + + // all other matches requires valid value + + guard let value = value else { + throw OptimizelyError.userAttributeNilValue(stringRepresentation) + } - if rawAttributeValue == nil { - throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal) + if matchFinal == .qualified { + guard case .string(let strValue) = value else { + throw OptimizelyError.evaluateAttributeInvalidCondition(stringRepresentation) } + return user.isQualifiedFor(segment: strValue) } + guard attributes.keys.contains(nameFinal) else { + throw OptimizelyError.missingAttributeValue(stringRepresentation, nameFinal) + } + + guard let rawAttributeValue = rawAttributeValue 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/Project.swift b/Sources/Data Model/Project.swift index 0aabc96a..2f9446da 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 @@ -70,7 +70,7 @@ struct Project: Codable, Equatable { 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 +78,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/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index ba40d7d8..8a8d4365 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) diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index b49f278f..20772c68 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -27,7 +27,20 @@ public class OptimizelyUserContext { } var forcedDecisions: AtomicDictionary? - public var qualifiedSegments: [String]? + + var atomicQualifiedSegments: AtomicArray? + public var qualifiedSegments: [String]? { + get { + return atomicQualifiedSegments?.property + } + set { + if let value = newValue { + atomicQualifiedSegments = AtomicArray(value) + } else { + atomicQualifiedSegments = nil + } + } + } var clone: OptimizelyUserContext? { guard let optimizely = self.optimizely else { return nil } @@ -37,6 +50,10 @@ public class OptimizelyUserContext { userContext.forcedDecisions = AtomicDictionary(fds.property) } + if let qs = atomicQualifiedSegments { + userContext.atomicQualifiedSegments = AtomicArray(qs.property) + } + return userContext } @@ -185,14 +202,14 @@ extension OptimizelyUserContext { return } - self.qualifiedSegments = segments + self.atomicQualifiedSegments = AtomicArray(segments) completionHandler(nil) } } // true if the user is qualified for the given segment name public func isQualifiedFor(segment: String) -> Bool { - return qualifiedSegments?.contains(segment) ?? false + return atomicQualifiedSegments?.contains(segment) ?? false } } 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/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-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/OptimizelySwiftSDKiOSTests.swift b/Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift index c49d452f..c96932aa 100644 --- a/Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift +++ b/Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift @@ -73,7 +73,7 @@ class OptimizelySDKTests: XCTestCase { } } - XCTAssertTrue(try! audience.conditionHolder.evaluate(project: config.project, attributes: attr)) + XCTAssertTrue(try! audience.conditionHolder.evaluate(project: config.project, user: OTUtils.user(attributes: attr))) } XCTAssertNotNil(config) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift index 837c1e09..6f7bdd88 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift @@ -25,7 +25,7 @@ class OptimizelyUserContextTests_Segments_Decide: XCTestCase { override func setUp() { let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! - optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, defaultLogLevel: .info) try! optimizely.start(datafile: datafile) user = optimizely.createUserContext(userId: kUserId) } 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/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..bcb5139f 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,64 @@ 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))) } } diff --git a/Tests/TestData/decide/decide_audience_segments.json b/Tests/TestData/decide/decide_audience_segments.json index eb289768..e1bd85a0 100644 --- a/Tests/TestData/decide/decide_audience_segments.json +++ b/Tests/TestData/decide/decide_audience_segments.json @@ -111,12 +111,12 @@ }, { "id": "13389142234", - "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"qualified\", \"name\": \"$opt_user_id\", \"type\": \"segment\", \"value\": \"odp-segement-1\"}]]]", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"qualified\", \"name\": \"odp.audiences\", \"type\": \"third_party_dimension\", \"value\": \"odp-segment-1\"}]]]", "name": "odp-segment" }, { "id": "13389130056", - "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"qualified\", \"name\": \"$opt_user_id\", \"type\": \"segment\", \"value\": \"odp-segement-2\"}]]]", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"qualified\", \"name\": \"odp.audiences\", \"type\": \"third_party_dimension\", \"value\": \"odp-segment-2\"}]]]", "name": "odp-segment" } ], diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index b22cdc5a..3a035492 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -310,6 +310,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], From e9a026b3b5bfd8512c2913b1acb9036c797f7acc Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 11 Apr 2022 17:47:06 -0700 Subject: [PATCH 10/84] add segments tests --- .../xcschemes/DemoSwiftwatchOS.xcscheme | 25 +-- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 6 + .../DefaultAudienceSegmentsHandler.swift | 11 +- Sources/AudienceSegments/SegmentsCache.swift | 2 +- .../AudienceSegments/ZaiusApiManager.swift | 66 ++++--- .../OptimizelyUserContext.swift | 14 +- Sources/Optimizely/OptimizelyClient.swift | 2 +- Sources/Optimizely/OptimizelyError.swift | 4 +- .../Protocols/OPTAudienceSegmentHandler.swift | 4 +- .../AudienceSegmentsHandlerTests.swift | 185 ++++++++++++++++++ .../OptimizelyUserContextTests_Segments.swift | 4 +- 11 files changed, 260 insertions(+), 63 deletions(-) create mode 100644 Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift diff --git a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme index 7aa2b41d..1ebde8af 100644 --- a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme +++ b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme @@ -54,10 +54,8 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - - - - - + diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 4e40ee67..265c1f85 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1771,6 +1771,8 @@ 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 */; }; + 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */; }; + 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.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 */; }; @@ -2241,6 +2243,7 @@ 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 = ""; }; + 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandlerTests.swift; sourceTree = ""; }; 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelySegmentOption.swift; sourceTree = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; @@ -2731,6 +2734,7 @@ 6E75197E22C5211100B2B157 /* OptimizelyTests-Common */ = { isa = PBXGroup; children = ( + 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */, 6E75197F22C5211100B2B157 /* MurmurTests.swift */, 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */, 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */, @@ -4528,6 +4532,7 @@ 6E7517D122C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6EA2CC2D2345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFAE24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, + 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */, 6E7517AB22C520D400B2B157 /* Array+Extension.swift in Sources */, 6E75186122C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E75172722C520D400B2B157 /* OptimizelyResult.swift in Sources */, @@ -4720,6 +4725,7 @@ 6E9B114722C5486E00C22D81 /* OptimizelyErrorTests.swift in Sources */, 84F6BABB27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B114B22C5486E00C22D81 /* BatchEventBuilderTest.swift in Sources */, + 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */, 6E9B115A22C5486E00C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B115422C5486E00C22D81 /* LoggerTests.swift in Sources */, 6E7518DF22C520D400B2B157 /* ConditionLeaf.swift in Sources */, diff --git a/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift b/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift index 8e2324ee..61d56c56 100644 --- a/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift @@ -1,5 +1,5 @@ // -// 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. @@ -25,16 +25,19 @@ class DefaultAudienceSegmentsHandler: OPTAudienceSegmentsHandler { static let reservedUserIdKey = "$opt_user_id" - let zaiusMgr = ZaiusApiManager() - let cache = SegmentsCache() + var zaiusMgr = ZaiusApiManager() + var cache = SegmentsCache() let logger = OPTLoggerFactory.getLogger() func fetchQualifiedSegments(apiKey: String, - userKey: String, + userKey: String?, userValue: String, segmentsToCheck: [String]? = nil, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + + let userKey = userKey ?? DefaultAudienceSegmentsHandler.reservedUserIdKey + zaiusMgr.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, diff --git a/Sources/AudienceSegments/SegmentsCache.swift b/Sources/AudienceSegments/SegmentsCache.swift index 7c3039b2..b4426368 100644 --- a/Sources/AudienceSegments/SegmentsCache.swift +++ b/Sources/AudienceSegments/SegmentsCache.swift @@ -1,5 +1,5 @@ // -// 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/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 5c974034..5a6af4ed 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -1,8 +1,17 @@ // -// GraphQL.swift -// SwiftGraphQL +// Copyright 2022, Optimizely, Inc. and contributors // -// Created by Jae Kim on 1/10/22. +// 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 @@ -74,7 +83,7 @@ class ZaiusApiManager { segmentsToCheck: [String]?, completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { if userKey != "vuid" { - self.logger.e("Currently userKeys other than 'vuid' are not supported yet.") + completionHandler([], .fetchSegmentsFailed("Currently userKeys other than 'vuid' are not supported yet.")) return } @@ -95,37 +104,46 @@ class ZaiusApiManager { urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") urlRequest.setValue(apiKey, forHTTPHeaderField: "x-api-key") - let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in + 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, _, error in if let error = error { - self.logger.e("GraphQL download failed: \(error)") + self.logger.d { + "GraphQL download failed: \(error)" + } + completionHandler([], .fetchSegmentsFailed("download failed")) return } - if let data = data { - if let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - if let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") { - let audiences = audDict.compactMap { ODPAudience($0["node"] as? [String: Any]) } - //print("[GraphQL Response] \(audiences)") - - let segments = audiences.filter { $0.isQualified }.map { $0.name } - //print("[GraphQL Audience Segments] \(segments)") - - completionHandler(segments, nil) - return - } - } else { - self.logger.e("GraphQL decode failed: " + String(bytes: data, encoding: .utf8)!) - } - } else { - self.logger.e("GraphQL data empty") + guard let data = data else { + completionHandler([], .fetchSegmentsFailed("response data empty")) + return } - completionHandler([], OptimizelyError.generic) + guard let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") + else { + self.logger.d { + "GraphQL decode failed: " + String(bytes: data, encoding: .utf8)! + } + completionHandler([], .fetchSegmentsFailed("decode error")) + return + } + + let audiences = audDict.compactMap { ODPAudience($0["node"] as? [String: Any]) } + let segments = audiences.filter { $0.isQualified }.map { $0.name } + completionHandler(segments, nil) } task.resume() } + func getSession() -> URLSession { + return URLSession(configuration: .ephemeral) + } + } struct ODPAudience: Decodable { diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 20772c68..1733e0c8 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -185,23 +185,19 @@ extension OptimizelyUserContext { return } - let userKey = userKey ?? DefaultAudienceSegmentsHandler.reservedUserIdKey let userValue = userValue ?? userId optimizely.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, options: options) { segments, err in - if let err = err { - completionHandler(err) + guard err == nil, let segments = segments else { + let error = err ?? OptimizelyError.fetchSegmentsFailed("invalid segments") + self.logger.e(error) + completionHandler(error) return } - - guard let segments = segments else { - completionHandler(.fetchSegmentsFailed("invalid segments")) - return - } - + self.atomicQualifiedSegments = AtomicArray(segments) completionHandler(nil) } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index f745a82d..c53a37b5 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -752,7 +752,7 @@ open class OptimizelyClient: NSObject { // MARK: - AudienceSegmentsHandler func fetchQualifiedSegments(apiKey: String, - userKey: String, + userKey: String?, userValue: String, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { diff --git a/Sources/Optimizely/OptimizelyError.swift b/Sources/Optimizely/OptimizelyError.swift index 060c7de6..206f4382 100644 --- a/Sources/Optimizely/OptimizelyError.swift +++ b/Sources/Optimizely/OptimizelyError.swift @@ -82,7 +82,7 @@ public enum OptimizelyError: Error { // MARK: - AudienceSegements Errors - case fetchSegmentsFailed(_ reason: String) + case fetchSegmentsFailed(_ hint: String) } // MARK: - CustomStringConvertible @@ -152,7 +152,7 @@ extension OptimizelyError: CustomStringConvertible, ReasonProtocol { case .eventDispatchFailed(let hint): message = "Event dispatch failed (\(hint))." case .eventDispatcherConfigError(let hint): message = "EventDispatcher config error (\(hint))." - case .fetchSegmentsFailed(let hint): message = "Fetch segments failed (\(hint))." + case .fetchSegmentsFailed(let hint): message = "Audience segments fetch failed (\(hint))." } return message diff --git a/Sources/Protocols/OPTAudienceSegmentHandler.swift b/Sources/Protocols/OPTAudienceSegmentHandler.swift index 6404fa9e..1d849fb7 100644 --- a/Sources/Protocols/OPTAudienceSegmentHandler.swift +++ b/Sources/Protocols/OPTAudienceSegmentHandler.swift @@ -1,5 +1,5 @@ // -// Copyright 2019-2022, 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. @@ -19,7 +19,7 @@ import Foundation protocol OPTAudienceSegmentsHandler { func fetchQualifiedSegments(apiKey: String, - userKey: String, + userKey: String?, userValue: String, segmentsToCheck: [String]?, options: [OptimizelySegmentOption], diff --git a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift new file mode 100644 index 00000000..da79c839 --- /dev/null +++ b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift @@ -0,0 +1,185 @@ +// +// 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 AudienceSegmentsHandlerTests: XCTestCase { + var handler = DefaultAudienceSegmentsHandler() + + // TODO: currently "vuid" only supported + //var userKey = "test-user-key" + var userKey = "vuid" + + var userValue = "test-user-value" + var apiKey = "test-api-key" + + override func setUp() { + } + + func testFetchQualifiedSegments_success() { + handler.zaiusMgr = MockZaiusApiManager(statusCode: 200) + + let sem = DispatchSemaphore(value: 0) + handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + segmentsToCheck: nil, options: []) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, ["qualified-and-ready"]) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_networkError() { + handler.zaiusMgr = MockZaiusApiManager(withError: true) + + let sem = DispatchSemaphore(value: 0) + handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + segmentsToCheck: nil, options: []) { _, error in + if case .fetchSegmentsFailed("download failed") = error { + XCTAssert(true) + } else { + XCTFail() + } + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_decodeError() { + handler.zaiusMgr = MockZaiusApiManager(statusCode: 200, responseData: "invalid-json") + + let sem = DispatchSemaphore(value: 0) + handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + segmentsToCheck: nil, options: []) { _, error in + if case .fetchSegmentsFailed("decode error") = error { + XCTAssert(true) + } else { + XCTFail() + } + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + +} + +// MARK: - MockZaiusApiManager + +class MockZaiusApiManager: ZaiusApiManager { + let mockUrlSession: MockZaiusUrlSession + + init(statusCode: Int = 0, withError: Bool = false, responseData: String? = nil) { + mockUrlSession = MockZaiusUrlSession(statusCode: statusCode, withError: withError, responseData: responseData) + } + + override func getSession() -> URLSession { + return mockUrlSession + } +} + +// MARK: - MockZaiusUrlSession + +class MockZaiusUrlSession: 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, withError: Bool, responseData: String?) { + Self.validSessions += 1 + self.statusCode = statusCode + self.withError = withError + self.responseData = responseData ?? MockZaiusUrlSession.goodResponseData + } + + override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + let headers = [String: String]() + + return MockDataTask() { + let statusCode = self.statusCode != 0 ? self.statusCode : 200 + let response = HTTPURLResponse(url: request.url!, + statusCode: statusCode, + httpVersion: nil, + headerFields: headers) + + let data = self.responseData?.data(using: .utf8) + let error = self.withError ? OptimizelyError.generic : nil + + completionHandler(data, response, error) + } + } + + override func finishTasksAndInvalidate() { + Self.validSessions -= 1 + } + + static let goodResponseData: String = """ + { + "data": { + "customer": { + "audiences": { + "edges": [ + { + "node": { + "name": "qualified-and-ready", + "is_ready": true, + "state": "qualified", + "description": "qualifed and ready" + } + }, + { + "node": { + "name": "qualified-and-not-ready", + "is_ready": false, + "state": "qualified", + "description": "qualified and not-ready" + } + }, + { + "node": { + "name": "not-qualified-and-ready", + "is_ready": false, + "state": "qualified", + "description": "not-qualified and ready" + } + }, + { + "node": { + "name": "not-qualified-and-not-ready", + "is_ready": false, + "state": "qualified", + "description": "not-qualified and not-ready" + } + } + ] + } + } + } + } + """ +} diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index 0bb5c574..7f8db399 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -21,6 +21,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { var optimizely: OptimizelyClient! var user: OptimizelyUserContext! let kUserId = "tester" + let kUserIdKey = "$opt_user_id" override func setUp() { optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) @@ -43,7 +44,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments(apiKey: MockAudienceSegmentsHandler.kApiKeyGood) { error in XCTAssertNil(error) - XCTAssert(self.user.qualifiedSegments == [MockAudienceSegmentsHandler.kApiKeyGood, "$opt_user_id", self.kUserId]) + XCTAssert(self.user.qualifiedSegments == [MockAudienceSegmentsHandler.kApiKeyGood, self.kUserIdKey, self.kUserId]) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -87,6 +88,7 @@ class MockAudienceSegmentsHandler: OPTAudienceSegmentsHandler { completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { if apiKey == MockAudienceSegmentsHandler.kApiKeyGood { + // pass back [key, userKey, userValue] in segments for validation let sampleSegments = [apiKey, userKey, userValue] completionHandler(sampleSegments, nil) } else { From e43cbf6138de44b726bb4e11500185320e5dfedb Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 13 Apr 2022 17:19:23 -0700 Subject: [PATCH 11/84] add tests for ATS --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 132 +++++----- .../DefaultAudienceSegmentsHandler.swift | 51 +++- Sources/AudienceSegments/LRUCache.swift | 133 ++++++++++ Sources/AudienceSegments/SegmentsCache.swift | 30 --- .../AudienceSegments/ZaiusApiManager.swift | 16 +- .../Data Model/Audience/UserAttribute.swift | 2 + .../DefaultDecisionService.swift | 2 +- .../Events/BatchEventBuilder.swift | 4 +- .../OptimizelySegmentOption.swift | 6 +- .../OptimizelyUserContext.swift | 3 +- Sources/Optimizely/OptimizelyClient.swift | 2 +- .../Protocols/OPTAudienceSegmentHandler.swift | 2 +- Sources/Utils/Constants.swift | 7 +- .../OptimizelyClientTests_Evaluation.swift | 8 +- .../AudienceSegmentsHandlerTests.swift | 185 ------------- .../BatchEventBuilderTests_Attributes.swift | 2 +- .../DefaultAudienceSegmentsHandlerTests.swift | 146 +++++++++++ .../LRUCacheTests.swift | 89 +++++++ .../OptimizelySwiftSDKiOSTests.swift | 89 ------- .../ZaiusApiManagerTests.swift | 246 ++++++++++++++++++ .../UserAttributeTests_Evaluate.swift | 38 +++ 21 files changed, 790 insertions(+), 403 deletions(-) create mode 100644 Sources/AudienceSegments/LRUCache.swift delete mode 100644 Sources/AudienceSegments/SegmentsCache.swift delete mode 100644 Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift create mode 100644 Tests/OptimizelyTests-Common/DefaultAudienceSegmentsHandlerTests.swift create mode 100644 Tests/OptimizelyTests-Common/LRUCacheTests.swift delete mode 100644 Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift create mode 100644 Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 265c1f85..92f20b20 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -657,22 +657,22 @@ 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E652300278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652301278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652302278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652303278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652304278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652305278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652306278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652307278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652308278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E652309278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E65230A278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E65230B278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E65230C278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E65230D278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E65230E278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */; }; - 6E65230F278E688B00954EA1 /* SegmentsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522FF278E688B00954EA1 /* SegmentsCache.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 */; }; @@ -1412,7 +1412,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 */; }; @@ -1436,7 +1435,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 */; }; @@ -1771,8 +1769,12 @@ 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 */; }; - 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */; }; - 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */; }; + 8428D3D02807337400D0FB0C /* LRUCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */; }; + 8428D3D12807337400D0FB0C /* LRUCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */; }; + 8428D3D5280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; + 8428D3D7280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; + 84632F1128049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift */; }; + 84632F1228049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.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 */; }; @@ -2055,7 +2057,7 @@ 6E6419FD265734C100C49555 /* ProjectConfigTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectConfigTests_MultiClients.swift; sourceTree = ""; }; 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusApiManager.swift; sourceTree = ""; }; 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAudienceSegmentsHandler.swift; sourceTree = ""; }; - 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentsCache.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 = ""; }; @@ -2147,7 +2149,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 = ""; }; @@ -2243,7 +2244,9 @@ 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 = ""; }; - 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandlerTests.swift; sourceTree = ""; }; + 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCacheTests.swift; sourceTree = ""; }; + 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiusApiManagerTests.swift; sourceTree = ""; }; + 84632F1028049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAudienceSegmentsHandlerTests.swift; sourceTree = ""; }; 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelySegmentOption.swift; sourceTree = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; @@ -2463,7 +2466,7 @@ children = ( 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */, 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, - 6E6522FF278E688B00954EA1 /* SegmentsCache.swift */, + 6E6522FF278E688B00954EA1 /* LRUCache.swift */, ); path = AudienceSegments; sourceTree = ""; @@ -2734,35 +2737,35 @@ 6E75197E22C5211100B2B157 /* OptimizelyTests-Common */ = { isa = PBXGroup; children = ( - 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */, - 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 */, + 84632F1028049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.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 */, 6E27EC9A266EF11000B4A6D4 /* OptimizelyDecisionTests.swift */, + 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */, 6EC6DD6824AE94820017D296 /* OptimizelyUserContextTests.swift */, 6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */, 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */, @@ -2770,6 +2773,7 @@ 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */, 6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */, 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */, + 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */, 6E86CEB224FF20DE005DAFED /* OptimizelyUserContextTests_Objc.m */, ); path = "OptimizelyTests-Common"; @@ -3980,7 +3984,7 @@ 6E14CD7A2423F98D00010234 /* OPTUserProfileService.swift in Sources */, 6E14CDA32423F9C300010234 /* Constants.swift in Sources */, 6E4544B1270E67C800F2CEBC /* NetworkReachability.swift in Sources */, - 6E652307278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E652307278E688B00954EA1 /* LRUCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4057,7 +4061,7 @@ 84B4D75627E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E424D7626324DBD0081004A /* AtomicArrayTests.swift in Sources */, 6E424CD326324B270081004A /* OptimizelyError.swift in Sources */, - 6E652306278E688B00954EA1 /* SegmentsCache.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 */, @@ -4151,7 +4155,7 @@ 6E7516CB22C520D400B2B157 /* OPTLogger.swift in Sources */, 6E7517A322C520D400B2B157 /* Array+Extension.swift in Sources */, 6EC6DD4224ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, - 6E652301278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E652301278E688B00954EA1 /* LRUCache.swift in Sources */, 6E75193122C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75190D22C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75194922C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -4263,7 +4267,7 @@ 6E75194422C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75179222C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E4544B6270E67C800F2CEBC /* NetworkReachability.swift in Sources */, - 6E65230C278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E65230C278E688B00954EA1 /* LRUCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4320,7 +4324,7 @@ 6E9B11D722C548A200C22D81 /* OptimizelyErrorTests.swift in Sources */, C78CAF8624485029009FE876 /* OptimizelyClientTests_OptimizelyJSON_Objc.m in Sources */, 6E75194C22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 6E652308278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E652308278E688B00954EA1 /* LRUCache.swift in Sources */, 6E7517C022C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75183822C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75175222C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4419,7 +4423,7 @@ 6E75187722C520D400B2B157 /* Variation.swift in Sources */, 6E7517F322C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E7518FB22C520D500B2B157 /* UserAttribute.swift in Sources */, - 6E65230B278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E65230B278E688B00954EA1 /* LRUCache.swift in Sources */, 6E9B11B222C5489400C22D81 /* OTUtils.swift in Sources */, 6E7518D722C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516AD22C520D400B2B157 /* DefaultLogger.swift in Sources */, @@ -4513,6 +4517,7 @@ 6E7516C722C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E9B11B522C5489600C22D81 /* MockUrlSession.swift in Sources */, 6EF8DE1724BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, + 8428D3D12807337400D0FB0C /* LRUCacheTests.swift in Sources */, 6E7517C522C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190922C520D500B2B157 /* Attribute.swift in Sources */, 6E75177B22C520D400B2B157 /* SDKVersion.swift in Sources */, @@ -4526,21 +4531,21 @@ 6E7516EB22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75188522C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E75176F22C520D400B2B157 /* Utils.swift in Sources */, - 6E65230D278E688B00954EA1 /* SegmentsCache.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 */, - 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */, + 84632F1228049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift in Sources */, 6E7517AB22C520D400B2B157 /* Array+Extension.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 */, 6E424BE9263228E90081004A /* AtomicArray.swift in Sources */, + 8428D3D7280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */, 6E9B117222C5487100C22D81 /* EventDispatcherTests.swift in Sources */, 6E9B116922C5487100C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E75192122C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, @@ -4615,7 +4620,7 @@ 6E75170422C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187A22C520D400B2B157 /* Variation.swift in Sources */, 0B97DD9A249D332C003DE606 /* SemanticVersionTests.swift in Sources */, - 6E65230E278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E65230E278E688B00954EA1 /* LRUCache.swift in Sources */, 6E9B119C22C5488300C22D81 /* ProjectConfigTests.swift in Sources */, 6E7518FE22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7517F622C520D400B2B157 /* DataStoreMemory.swift in Sources */, @@ -4725,7 +4730,7 @@ 6E9B114722C5486E00C22D81 /* OptimizelyErrorTests.swift in Sources */, 84F6BABB27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B114B22C5486E00C22D81 /* BatchEventBuilderTest.swift in Sources */, - 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */, + 84632F1128049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift in Sources */, 6E9B115A22C5486E00C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B115422C5486E00C22D81 /* LoggerTests.swift in Sources */, 6E7518DF22C520D400B2B157 /* ConditionLeaf.swift in Sources */, @@ -4761,13 +4766,14 @@ 6E7516D922C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E7516E522C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7E9B552523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */, - 6E652305278E688B00954EA1 /* SegmentsCache.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 */, + 8428D3D5280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */, 6E7517EF22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E75194B22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75195722C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -4781,7 +4787,6 @@ 6E7516B522C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7516A922C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E6522BF278DF27300954EA1 /* ZaiusApiManager.swift in Sources */, - 6E9B114822C5486E00C22D81 /* OptimizelySwiftSDKiOSTests.swift in Sources */, 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */, @@ -4840,6 +4845,7 @@ 6E75193F22C520D500B2B157 /* OPTDecisionService.swift in Sources */, 84E7ABC027D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E7516CD22C520D400B2B157 /* OPTLogger.swift in Sources */, + 8428D3D02807337400D0FB0C /* LRUCacheTests.swift in Sources */, 6E7517FB22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4860,7 +4866,7 @@ 6E7516FF22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187522C520D400B2B157 /* Variation.swift in Sources */, 0B97DD99249D332C003DE606 /* SemanticVersionTests.swift in Sources */, - 6E652309278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E652309278E688B00954EA1 /* LRUCache.swift in Sources */, 6E9B118622C5488100C22D81 /* ProjectConfigTests.swift in Sources */, 6E7518F922C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7517F122C520D400B2B157 /* DataStoreMemory.swift in Sources */, @@ -5045,7 +5051,7 @@ 6E7517A822C520D400B2B157 /* Array+Extension.swift in Sources */, 6E7518EE22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E75185222C520D400B2B157 /* ProjectConfig.swift in Sources */, - 6E65230A278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E65230A278E688B00954EA1 /* LRUCache.swift in Sources */, C78CAF5F2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E623F0A253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75179022C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, @@ -5139,7 +5145,7 @@ 6E7517AD22C520D400B2B157 /* Array+Extension.swift in Sources */, 6E7518F322C520D500B2B157 /* ConditionHolder.swift in Sources */, 6E75185722C520D400B2B157 /* ProjectConfig.swift in Sources */, - 6E65230F278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E65230F278E688B00954EA1 /* LRUCache.swift in Sources */, C78CAF642445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E623F0F253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75179522C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, @@ -5212,7 +5218,7 @@ 6E75184C22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E75181022C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD4124ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, - 6E652300278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E652300278E688B00954EA1 /* LRUCache.swift in Sources */, 6E7516EE22C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75183422C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75171222C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -5324,7 +5330,7 @@ 6E75193E22C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75178C22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E4544AE270E67C800F2CEBC /* NetworkReachability.swift in Sources */, - 6E652304278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E652304278E688B00954EA1 /* LRUCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5358,7 +5364,7 @@ 75C71A1325E454460084187E /* OPTEventDispatcher.swift in Sources */, 75C71A1425E454460084187E /* DefaultDatafileHandler.swift in Sources */, 6E424BE0263228E90081004A /* AtomicArray.swift in Sources */, - 6E652303278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E652303278E688B00954EA1 /* LRUCache.swift in Sources */, 75C71A1525E454460084187E /* DecisionInfo.swift in Sources */, 75C71A1625E454460084187E /* DefaultBucketer.swift in Sources */, 75C71A1725E454460084187E /* DefaultNotificationCenter.swift in Sources */, @@ -5480,7 +5486,7 @@ BD64856B2491474500F30986 /* ProjectConfig.swift in Sources */, BD64856C2491474500F30986 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD4324ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, - 6E652302278E688B00954EA1 /* SegmentsCache.swift in Sources */, + 6E652302278E688B00954EA1 /* LRUCache.swift in Sources */, BD64856D2491474500F30986 /* OptimizelyError.swift in Sources */, BD64856E2491474500F30986 /* EventForDispatch.swift in Sources */, BD64856F2491474500F30986 /* OptimizelyClient+ObjC.swift in Sources */, diff --git a/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift b/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift index 61d56c56..c126a1d5 100644 --- a/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift @@ -17,33 +17,60 @@ import Foundation import UIKit -class DefaultAudienceSegmentsHandler: OPTAudienceSegmentsHandler { - +public class DefaultAudienceSegmentsHandler: OPTAudienceSegmentsHandler { // configurable size + timeout - static var cacheMaxSize = 1000 - static var cacheTimeoutInSecs = 10*60 - - static let reservedUserIdKey = "$opt_user_id" - + public static var cacheMaxSize = 100 + public static var cacheTimeoutInSecs = 10*60 + var zaiusMgr = ZaiusApiManager() - var cache = SegmentsCache() + var segmentsCache = LRUCache(size: DefaultAudienceSegmentsHandler.cacheMaxSize, + timeoutInSecs: DefaultAudienceSegmentsHandler.cacheTimeoutInSecs) let logger = OPTLoggerFactory.getLogger() - + func fetchQualifiedSegments(apiKey: String, - userKey: String?, + userKey: String, userValue: String, segmentsToCheck: [String]? = nil, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - - let userKey = userKey ?? DefaultAudienceSegmentsHandler.reservedUserIdKey + let cacheKey = cacheKey(userKey, userValue) + let ignoreCache = options.contains(.ignoreCache) + let resetCache = options.contains(.resetCache) + + if resetCache { + segmentsCache.reset() + } + + if !ignoreCache { + if let segments = segmentsCache.lookup(key: cacheKey) { + completionHandler(segments, nil) + return + } + } + zaiusMgr.fetch(apiKey: apiKey, 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) } } } + +// MARK: - Utils + +extension DefaultAudienceSegmentsHandler { + + func cacheKey(_ userKey: String, _ userValue: String) -> String { + return userKey + "-$-" + userValue + } + +} diff --git a/Sources/AudienceSegments/LRUCache.swift b/Sources/AudienceSegments/LRUCache.swift new file mode 100644 index 00000000..b6fdf8af --- /dev/null +++ b/Sources/AudienceSegments/LRUCache.swift @@ -0,0 +1,133 @@ +// +// 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 size: Int + let timeoutInSecs: Int + + #if DEBUG + let minSize = 1 + let minTimeoutInSecs = 1 + #else + let minSize = 10 + let minTimeoutInSecs = 60 + #endif + + init(size: Int, timeoutInSecs: Int) { + self.size = max(size, minSize) + self.timeoutInSecs = max(timeoutInSecs, minTimeoutInSecs) + self.reset() + } + + func lookup(key: K) -> V? { + var element: CacheElement? = nil + 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) { + 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.size { + 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? { + var element: CacheElement? = nil + queue.sync { + element = map[key] + } + return element?.value + } + + func reset() { + 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 { + return (Date.timeIntervalSinceReferenceDate - item.time) < Double(timeoutInSecs) + } + +} diff --git a/Sources/AudienceSegments/SegmentsCache.swift b/Sources/AudienceSegments/SegmentsCache.swift deleted file mode 100644 index b4426368..00000000 --- a/Sources/AudienceSegments/SegmentsCache.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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 SegmentsCache { - var map = [String: [String]]() - var order = [String]() - - func lookup(userId: String) -> [String]? { - return map[userId] - } - - func save(userId: String, segments: [String]) { - map[userId] = segments - } -} diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 5a6af4ed..f4777170 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -21,6 +21,8 @@ import Foundation /* GraphQL Request +curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name is_ready state description}}}}}"}' https://api.zaius.com/v3/graphql + query MyQuery { customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { audiences { @@ -33,12 +35,12 @@ query MyQuery { } } } - vuid } } */ /* GraphQL Response + { "data": { "customer": { @@ -73,10 +75,6 @@ class ZaiusApiManager { let apiHost = "https://api.zaius.com/v3/graphql" - /* - curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name is_ready state description}}} vuid}}"}' https://api.zaius.com/v3/graphql - */ - func fetch(apiKey: String, userKey: String, userValue: String, @@ -94,11 +92,13 @@ class ZaiusApiManager { let body = [ "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state description}}}}}" ] - guard let httpBody = try? JSONEncoder().encode(body) else { return } + guard let httpBody = try? JSONEncoder().encode(body) else { + completionHandler([], .fetchSegmentsFailed("Invalid query.")) + return + } let url = URL(string: apiHost)! var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = "POST" urlRequest.httpBody = httpBody urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") @@ -186,7 +186,7 @@ extension Dictionary { } } - return current as? T + return current == nil ? nil : (current as? T) } } diff --git a/Sources/Data Model/Audience/UserAttribute.swift b/Sources/Data Model/Audience/UserAttribute.swift index cdfc8b66..7a6beb34 100644 --- a/Sources/Data Model/Audience/UserAttribute.swift +++ b/Sources/Data Model/Audience/UserAttribute.swift @@ -130,6 +130,8 @@ extension UserAttribute { } 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) } diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 8a8d4365..276d3a15 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -417,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 6a95d39a..ae526235 100644 --- a/Sources/Implementation/Events/BatchEventBuilder.swift +++ b/Sources/Implementation/Events/BatchEventBuilder.swift @@ -204,9 +204,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/Optimizely+Decide/OptimizelySegmentOption.swift b/Sources/Optimizely+Decide/OptimizelySegmentOption.swift index 926f71a9..dd186ca1 100644 --- a/Sources/Optimizely+Decide/OptimizelySegmentOption.swift +++ b/Sources/Optimizely+Decide/OptimizelySegmentOption.swift @@ -18,6 +18,8 @@ import Foundation /// Options controlling audience segments. @objc public enum OptimizelySegmentOption: Int { - // refresh the cache for the current target user - case forceCacheRefresh + // ignore cache (save/lookup) + case ignoreCache + // reset cache + case resetCache } diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 1733e0c8..e245a5f8 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -185,8 +185,9 @@ extension OptimizelyUserContext { return } + let userKey = userKey ?? Constants.Attributes.reservedUserIdKey let userValue = userValue ?? userId - + optimizely.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index c53a37b5..f745a82d 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -752,7 +752,7 @@ open class OptimizelyClient: NSObject { // MARK: - AudienceSegmentsHandler func fetchQualifiedSegments(apiKey: String, - userKey: String?, + userKey: String, userValue: String, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { diff --git a/Sources/Protocols/OPTAudienceSegmentHandler.swift b/Sources/Protocols/OPTAudienceSegmentHandler.swift index 1d849fb7..c3135faa 100644 --- a/Sources/Protocols/OPTAudienceSegmentHandler.swift +++ b/Sources/Protocols/OPTAudienceSegmentHandler.swift @@ -19,7 +19,7 @@ import Foundation protocol OPTAudienceSegmentsHandler { func fetchQualifiedSegments(apiKey: String, - userKey: String?, + userKey: String, userValue: String, segmentsToCheck: [String]?, options: [OptimizelySegmentOption], diff --git a/Sources/Utils/Constants.swift b/Sources/Utils/Constants.swift index 04ccb1fa..2ccd6435 100644 --- a/Sources/Utils/Constants.swift +++ b/Sources/Utils/Constants.swift @@ -18,9 +18,10 @@ 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" + static let reservedUserIdKey = "$opt_user_id" } enum EvaluationLogType: String { diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift index bd1e3701..6be76fd3 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift @@ -34,7 +34,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) @@ -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-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift deleted file mode 100644 index da79c839..00000000 --- a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// 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 AudienceSegmentsHandlerTests: XCTestCase { - var handler = DefaultAudienceSegmentsHandler() - - // TODO: currently "vuid" only supported - //var userKey = "test-user-key" - var userKey = "vuid" - - var userValue = "test-user-value" - var apiKey = "test-api-key" - - override func setUp() { - } - - func testFetchQualifiedSegments_success() { - handler.zaiusMgr = MockZaiusApiManager(statusCode: 200) - - let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: []) { segments, error in - XCTAssertNil(error) - XCTAssertEqual(segments, ["qualified-and-ready"]) - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - } - - func testFetchQualifiedSegments_networkError() { - handler.zaiusMgr = MockZaiusApiManager(withError: true) - - let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: []) { _, error in - if case .fetchSegmentsFailed("download failed") = error { - XCTAssert(true) - } else { - XCTFail() - } - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - } - - func testFetchQualifiedSegments_decodeError() { - handler.zaiusMgr = MockZaiusApiManager(statusCode: 200, responseData: "invalid-json") - - let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: []) { _, error in - if case .fetchSegmentsFailed("decode error") = error { - XCTAssert(true) - } else { - XCTFail() - } - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - } - -} - -// MARK: - MockZaiusApiManager - -class MockZaiusApiManager: ZaiusApiManager { - let mockUrlSession: MockZaiusUrlSession - - init(statusCode: Int = 0, withError: Bool = false, responseData: String? = nil) { - mockUrlSession = MockZaiusUrlSession(statusCode: statusCode, withError: withError, responseData: responseData) - } - - override func getSession() -> URLSession { - return mockUrlSession - } -} - -// MARK: - MockZaiusUrlSession - -class MockZaiusUrlSession: 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, withError: Bool, responseData: String?) { - Self.validSessions += 1 - self.statusCode = statusCode - self.withError = withError - self.responseData = responseData ?? MockZaiusUrlSession.goodResponseData - } - - override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - let headers = [String: String]() - - return MockDataTask() { - let statusCode = self.statusCode != 0 ? self.statusCode : 200 - let response = HTTPURLResponse(url: request.url!, - statusCode: statusCode, - httpVersion: nil, - headerFields: headers) - - let data = self.responseData?.data(using: .utf8) - let error = self.withError ? OptimizelyError.generic : nil - - completionHandler(data, response, error) - } - } - - override func finishTasksAndInvalidate() { - Self.validSessions -= 1 - } - - static let goodResponseData: String = """ - { - "data": { - "customer": { - "audiences": { - "edges": [ - { - "node": { - "name": "qualified-and-ready", - "is_ready": true, - "state": "qualified", - "description": "qualifed and ready" - } - }, - { - "node": { - "name": "qualified-and-not-ready", - "is_ready": false, - "state": "qualified", - "description": "qualified and not-ready" - } - }, - { - "node": { - "name": "not-qualified-and-ready", - "is_ready": false, - "state": "qualified", - "description": "not-qualified and ready" - } - }, - { - "node": { - "name": "not-qualified-and-not-ready", - "is_ready": false, - "state": "qualified", - "description": "not-qualified and not-ready" - } - } - ] - } - } - } - } - """ -} 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/DefaultAudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/DefaultAudienceSegmentsHandlerTests.swift new file mode 100644 index 00000000..b636d046 --- /dev/null +++ b/Tests/OptimizelyTests-Common/DefaultAudienceSegmentsHandlerTests.swift @@ -0,0 +1,146 @@ +// +// 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 DefaultAudienceSegmentsHandlerTests: XCTestCase { + var handler = DefaultAudienceSegmentsHandler() + var options = [OptimizelySegmentOption]() + + var apiKey = "valid" + var userKey = "vuid" + var userValue = "test-user" + + override func setUp() { + handler.zaiusMgr = MockZaiusApiManager() + } + + func testSuccess_cacheMiss() { + setCache(userKey, "123", ["a"]) + + let sem = DispatchSemaphore(value: 0) + + handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + segmentsToCheck: nil, options: options) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(["new-customer"], segments) + sem.signal() + } + + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + func testSuccess_cacheHit() { + setCache(userKey, userValue, ["a"]) + + let sem = DispatchSemaphore(value: 0) + + handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + segmentsToCheck: nil, options: options) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(["a"], segments) + sem.signal() + } + + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + func testOptions_ignoreCache() { + setCache(userKey, userValue, ["a"]) + options = [.ignoreCache] + + let sem = DispatchSemaphore(value: 0) + + handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + segmentsToCheck: nil, 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(30))) + } + + func testOptions_resetCache() { + setCache(userKey, userValue, ["a"]) + setCache(userKey, "123", ["a"]) + setCache(userKey, "456", ["a"]) + options = [.resetCache] + + let sem = DispatchSemaphore(value: 0) + + handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + segmentsToCheck: nil, 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(30))) + } + + func testError() { + let sem = DispatchSemaphore(value: 0) + + handler.fetchQualifiedSegments(apiKey: "invalid-key", userKey: userKey, userValue: userValue, + segmentsToCheck: nil, options: []) { segments, error in + XCTAssertNotNil(error) + XCTAssert(segments!.isEmpty ) + sem.signal() + } + + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + // MARK: - Utils + + func setCache(_ userKey: String, _ userValue: String, _ value: [String]) { + let cacheKey = handler.cacheKey(userKey, userValue) + handler.segmentsCache.save(key: cacheKey, value: value) + } + + func peekCache(_ userKey: String, _ userValue: String) -> [String]? { + let cacheKey = handler.cacheKey(userKey, userValue) + return handler.segmentsCache.peek(key: cacheKey) + } + + var cacheCount: Int { + return handler.segmentsCache.map.count + } + + // MARK: - MockZaiusApiManager + + class MockZaiusApiManager: ZaiusApiManager { + override func fetch(apiKey: String, + userKey: String, + userValue: String, + segmentsToCheck: [String]?, + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + DispatchQueue.global().async { + if apiKey == "invalid-key" { + completionHandler([], OptimizelyError.fetchSegmentsFailed("invalid key")) + } else { + completionHandler(["new-customer"], nil) + } + } + } + } + +} + diff --git a/Tests/OptimizelyTests-Common/LRUCacheTests.swift b/Tests/OptimizelyTests-Common/LRUCacheTests.swift new file mode 100644 index 00000000..9eaf35ec --- /dev/null +++ b/Tests/OptimizelyTests-Common/LRUCacheTests.swift @@ -0,0 +1,89 @@ +// +// 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.size) + XCTAssertEqual(2000, cache.timeoutInSecs) + + cache = LRUCache(size: 0, timeoutInSecs: 0) + XCTAssertEqual(1, cache.size) + XCTAssertEqual(1, 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.size) + } + + func testTimeout() { + let maxTimeout = 1 + let cache = LRUCache(size: 1000, timeoutInSecs: maxTimeout) + + cache.save(key: 1, value: 100) // [1] + cache.save(key: 2, value: 200) // [1, 2] + cache.save(key: 3, value: 200) // [1, 2] + sleep(2) + 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)) + } + +} diff --git a/Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift b/Tests/OptimizelyTests-Common/OptimizelySwiftSDKiOSTests.swift deleted file mode 100644 index c96932aa..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, user: OTUtils.user(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/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift new file mode 100644 index 00000000..16811401 --- /dev/null +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -0,0 +1,246 @@ +// +// 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 ZaiusApiManagerTests: XCTestCase { + + // TODO: currently "vuid" only supported + //var userKey = "test-user-key" + let userKey = "vuid" + + let userValue = "test-user-value" + let apiKey = "test-api-key" + + static var createdApiRequest: URLRequest? + + func testFetchQualifiedSegments_success() { + let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodResponseData)) + + let sem = DispatchSemaphore(value: 0) + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, ["qualified-and-ready"]) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_successWithEmptySegments() { + let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodEmptyResponseData)) + + let sem = DispatchSemaphore(value: 0) + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, []) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_badResponse() { + let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) + + let sem = DispatchSemaphore(value: 0) + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, error in + if case .fetchSegmentsFailed("decode error") = error { + XCTAssert(true) + } else { + XCTFail() + } + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_networkError() { + let manager = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) + + let sem = DispatchSemaphore(value: 0) + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, error in + if case .fetchSegmentsFailed("download failed") = error { + XCTAssert(true) + } else { + XCTFail() + } + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testGraphQLRequest() { + let manager = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) + + let sem = DispatchSemaphore(value: 0) + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, error in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + guard let request = ZaiusApiManagerTests.createdApiRequest else { + XCTFail() + return + } + + let expectedBody = [ + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state description}}}}}" + ] + + XCTAssertEqual("POST", request.httpMethod) + XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) + XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) + XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) + } + + 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: - MockZaiusApiManager + + class MockZaiusApiManager: ZaiusApiManager { + let mockUrlSession: URLSession + + init(_ urlSession: URLSession) { + mockUrlSession = urlSession + } + + override func getSession() -> URLSession { + return mockUrlSession + } + } + + // MARK: - MockZaiusUrlSession + + class MockZaiusUrlSession: 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 ?? MockZaiusUrlSession.goodResponseData + } + + override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + ZaiusApiManagerTests.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": "qualified-and-ready", + "is_ready": true, + "state": "qualified", + "description": "qualifed and ready" + } + }, + { + "node": { + "name": "qualified-and-not-ready", + "is_ready": false, + "state": "qualified", + "description": "qualified and not-ready" + } + }, + { + "node": { + "name": "not-qualified-and-ready", + "is_ready": false, + "state": "qualified", + "description": "not-qualified and ready" + } + }, + { + "node": { + "name": "not-qualified-and-not-ready", + "is_ready": false, + "state": "qualified", + "description": "not-qualified and not-ready" + } + } + ] + } + } + } + } + """ + + static let goodEmptyResponseData: String = """ + { + "data": { + "customer": { + "audiences": { + "edges": [] + } + } + } + } + """ + + static let badResponseData: String = """ + { + "data": {} + } + """ + + } + +} diff --git a/Tests/OptimizelyTests-DataModel/UserAttributeTests_Evaluate.swift b/Tests/OptimizelyTests-DataModel/UserAttributeTests_Evaluate.swift index bcb5139f..428eb6ab 100644 --- a/Tests/OptimizelyTests-DataModel/UserAttributeTests_Evaluate.swift +++ b/Tests/OptimizelyTests-DataModel/UserAttributeTests_Evaluate.swift @@ -460,3 +460,41 @@ extension UserAttributeTests_Evaluate { } } + +// 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") + } + +} From 553a3506598069587c04f57dd9e11984f63fa89c Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 14 Apr 2022 15:24:09 -0700 Subject: [PATCH 12/84] add segmentsToCheck filtering --- Sources/AudienceSegments/LRUCache.swift | 16 +++++ .../AudienceSegments/ZaiusApiManager.swift | 4 +- Sources/Data Model/Audience/Audience.swift | 28 ++++++++ Sources/Data Model/ProjectConfig.swift | 6 ++ Sources/Optimizely/OptimizelyClient.swift | 3 +- .../LRUCacheTests.swift | 15 ++++- .../OptimizelyUserContextTests_Segments.swift | 53 +++++++++++---- .../ZaiusApiManagerTests.swift | 2 +- .../AudienceTests.swift | 37 +++++++++++ .../ProjectConfigTests.swift | 10 +++ .../decide/decide_audience_segments.json | 65 ++++++++++++++++--- 11 files changed, 210 insertions(+), 29 deletions(-) diff --git a/Sources/AudienceSegments/LRUCache.swift b/Sources/AudienceSegments/LRUCache.swift index b6fdf8af..836cb203 100644 --- a/Sources/AudienceSegments/LRUCache.swift +++ b/Sources/AudienceSegments/LRUCache.swift @@ -55,6 +55,8 @@ class LRUCache { func lookup(key: K) -> V? { var element: CacheElement? = nil + var needReset = false + queue.sync { element = map[key] @@ -66,9 +68,18 @@ class LRUCache { } else { map[key] = nil element = nil + + // check if all are stale and can be reset. + needReset = !isAllStale() } } } + + // reset is called outside the sync block to avoid deadlock + if needReset { + reset() + } + return element?.value } @@ -129,5 +140,10 @@ class LRUCache { private func isValid(_ item: CacheElement) -> Bool { return (Date.timeIntervalSinceReferenceDate - item.time) < Double(timeoutInSecs) } + + private func isAllStale() -> Bool { + guard let mostRecent = tail.prev else { return false } + return (Date.timeIntervalSinceReferenceDate - mostRecent.time) < Double(timeoutInSecs) + } } diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index f4777170..c9fd8fb0 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -21,7 +21,7 @@ import Foundation /* GraphQL Request -curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name is_ready state description}}}}}"}' https://api.zaius.com/v3/graphql +curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name is_ready state}}}}}"}' https://api.zaius.com/v3/graphql query MyQuery { customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { @@ -90,7 +90,7 @@ class ZaiusApiManager { } let body = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state description}}}}}" + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state}}}}}" ] guard let httpBody = try? JSONEncoder().encode(body) else { completionHandler([], .fetchSegmentsFailed("Invalid query.")) diff --git a/Sources/Data Model/Audience/Audience.swift b/Sources/Data Model/Audience/Audience.swift index e502423c..252b6218 100644 --- a/Sources/Data Model/Audience/Audience.swift +++ b/Sources/Data Model/Audience/Audience.swift @@ -73,5 +73,33 @@ struct Audience: Codable, Equatable, OptimizelyAudience { 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/ProjectConfig.swift b/Sources/Data Model/ProjectConfig.swift index 44d7f42d..7fc988f1 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 @@ -146,6 +147,11 @@ class ProjectConfig { return map }() + self.allSegments = { + let audiences = project.typedAudiences ?? [] + return audiences.flatMap { $0.getSegments() } + }() + } func getAllRulesForFlag(_ flag: FeatureFlag) -> [Experiment] { diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index f745a82d..54723d8d 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -759,8 +759,7 @@ open class OptimizelyClient: NSObject { // instantiate on the first request let handler = audienceSegmentsHandler ?? DefaultAudienceSegmentsHandler() - //let segmentsToCheck = getAllSegmentsForProject() - let segmentsToCheck = [String]() + let segmentsToCheck = config?.allSegments handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, diff --git a/Tests/OptimizelyTests-Common/LRUCacheTests.swift b/Tests/OptimizelyTests-Common/LRUCacheTests.swift index 9eaf35ec..13337826 100644 --- a/Tests/OptimizelyTests-Common/LRUCacheTests.swift +++ b/Tests/OptimizelyTests-Common/LRUCacheTests.swift @@ -75,7 +75,7 @@ class LRUCacheTests: XCTestCase { cache.save(key: 1, value: 100) // [1] cache.save(key: 2, value: 200) // [1, 2] - cache.save(key: 3, value: 200) // [1, 2] + cache.save(key: 3, value: 300) // [1, 2, 3] sleep(2) cache.save(key: 4, value: 400) // [1, 2, 3] cache.save(key: 1, value: 101) // [1] @@ -85,5 +85,18 @@ class LRUCacheTests: XCTestCase { XCTAssertNil(cache.lookup(key: 3)) XCTAssertEqual(400, cache.lookup(key: 4)) } + + func testAllStale() { + let maxTimeout = 1 + let cache = LRUCache(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) + + XCTAssertNil(cache.lookup(key: 1)) + XCTAssert(cache.map.isEmpty, "cache should be reset when detected that all items are stale") + } } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index 7f8db399..f89cfd0e 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -18,14 +18,15 @@ import XCTest class OptimizelyUserContextTests_Segments: XCTestCase { - var optimizely: OptimizelyClient! + var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + var segmentHandler = MockAudienceSegmentsHandler() var user: OptimizelyUserContext! + let kApiKey = "any-key" let kUserId = "tester" let kUserIdKey = "$opt_user_id" override func setUp() { - optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - optimizely.audienceSegmentsHandler = MockAudienceSegmentsHandler() + optimizely.audienceSegmentsHandler = segmentHandler user = optimizely.createUserContext(userId: kUserId) } @@ -42,9 +43,9 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_successDefaultUser() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: MockAudienceSegmentsHandler.kApiKeyGood) { error in + user.fetchQualifiedSegments(apiKey: kApiKey) { error in XCTAssertNil(error) - XCTAssert(self.user.qualifiedSegments == [MockAudienceSegmentsHandler.kApiKeyGood, self.kUserIdKey, self.kUserId]) + XCTAssert(self.user.qualifiedSegments == [self.kApiKey, self.kUserIdKey, self.kUserId]) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -54,7 +55,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { user.optimizely = nil let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: MockAudienceSegmentsHandler.kApiKeyGood) { error in + user.fetchQualifiedSegments(apiKey: kApiKey) { error in XCTAssertEqual(OptimizelyError.sdkNotReady.reason, error?.reason) XCTAssertNil(self.user.qualifiedSegments) sem.signal() @@ -64,22 +65,46 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_fetchFailed() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: MockAudienceSegmentsHandler.kApiKeyBad) { error in + user.fetchQualifiedSegments(apiKey: "invalid-key") { error in XCTAssertNotNil(error) XCTAssertNil(self.user.qualifiedSegments) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) } + + func testFetchQualifiedSegments_segmentsToCheck_emptyBeforeStart() { + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: kApiKey) { error in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + + XCTAssertNil(segmentHandler.segmentsToCheck) + } + + func testFetchQualifiedSegments_segmentsToCheck_validAfterStart() { + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + try? optimizely.start(datafile: datafile) + + // fetch segments after SDK initialized, so segmentsToCheck will be used. + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: kApiKey) { error in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!)) + } } // MARK: - MockAudienceSegmentsHandler class MockAudienceSegmentsHandler: OPTAudienceSegmentsHandler { - static let kApiKeyGood = "apiKeyGood" - static let kApiKeyBad = "apiKeyBad" - + var segmentsToCheck: [String]? + func fetchQualifiedSegments(apiKey: String, userKey: String, userValue: String, @@ -87,12 +112,14 @@ class MockAudienceSegmentsHandler: OPTAudienceSegmentsHandler { options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - if apiKey == MockAudienceSegmentsHandler.kApiKeyGood { + self.segmentsToCheck = segmentsToCheck + + if apiKey == "invalid-key" { + completionHandler(nil, OptimizelyError.generic) + } else { // pass back [key, userKey, userValue] in segments for validation let sampleSegments = [apiKey, userKey, userValue] completionHandler(sampleSegments, nil) - } else { - completionHandler(nil, OptimizelyError.generic) } } } diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift index 16811401..31548fe2 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -96,7 +96,7 @@ class ZaiusApiManagerTests: XCTestCase { } let expectedBody = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state description}}}}}" + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state}}}}}" ] XCTAssertEqual("POST", request.httpMethod) diff --git a/Tests/OptimizelyTests-DataModel/AudienceTests.swift b/Tests/OptimizelyTests-DataModel/AudienceTests.swift index f5042175..56379918 100644 --- a/Tests/OptimizelyTests-DataModel/AudienceTests.swift +++ b/Tests/OptimizelyTests-DataModel/AudienceTests.swift @@ -127,3 +127,40 @@ extension AudienceTests { } } + +// MARK: - Others + +extension AudienceTests { + + func testGetSegments() { + let seg1 = ["name": "odp.audiences", "type": "third_party_dimension", "match": "qualified", "value": "seg1"] + let seg2 = ["name": "odp.audiences", "type": "third_party_dimension", "match": "qualified", "value": "seg2"] + let seg3 = ["name": "odp.audiences", "type": "third_party_dimension", "match": "qualified", "value": "seg3"] + let other = ["name": "other", "type": "custom_attribute", "match": "eq", "value": "a"] + + var audience = makeAudience([seg1]) + XCTAssertEqual(["seg1"], Set(audience.getSegments())) + + audience = makeAudience(["or", seg1]) + XCTAssertEqual(["seg1"], Set(audience.getSegments())) + + audience = makeAudience(["and", ["or", seg1]]) + XCTAssertEqual(["seg1"], Set(audience.getSegments())) + + audience = makeAudience(["and", ["or", seg1], ["or", seg2], ["and", other]]) + XCTAssertEqual(["seg1", "seg2"], Set(audience.getSegments())) + + audience = makeAudience(["and", ["or", seg1, other, seg2]]) + XCTAssertEqual(["seg1", "seg2"], Set(audience.getSegments())) + + audience = makeAudience(["and", ["or", seg1, other, seg2], ["and", seg1, seg2, seg3]]) + XCTAssertEqual(3, audience.getSegments().count) + XCTAssertEqual(["seg1", "seg2", "seg3"], Set(audience.getSegments())) + } + + func makeAudience(_ conditions: [Any]) -> Audience { + let data: [String: Any] = ["id": "12345", "name": "group-a", "conditions": conditions] + return try! OTUtils.model(from: data) + } + +} diff --git a/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift b/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift index afeb0a0b..a9c47270 100644 --- a/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift +++ b/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift @@ -97,6 +97,16 @@ 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) + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segments)) + } + } // MARK: - Others diff --git a/Tests/TestData/decide/decide_audience_segments.json b/Tests/TestData/decide/decide_audience_segments.json index e1bd85a0..0b9d4f24 100644 --- a/Tests/TestData/decide/decide_audience_segments.json +++ b/Tests/TestData/decide/decide_audience_segments.json @@ -103,21 +103,66 @@ } ], "groups": [], - "audiences": [ - { - "id": "13389141123", - "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"custom_attribute\", \"value\": 20}]]]", - "name": "adult" - }, + "typedAudiences": [ { "id": "13389142234", - "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"qualified\", \"name\": \"odp.audiences\", \"type\": \"third_party_dimension\", \"value\": \"odp-segment-1\"}]]]", - "name": "odp-segment" + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-segment-1" }, { "id": "13389130056", - "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"qualified\", \"name\": \"odp.audiences\", \"type\": \"third_party_dimension\", \"value\": \"odp-segment-2\"}]]]", - "name": "odp-segment" + "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": "equal" + } + ], + [ + "or", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-segment-2" + } + ], + "audiences": [ + { + "id": "13389141123", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"custom_attribute\", \"value\": 20}]]]", + "name": "adult" } ], "attributes": [ From cc28acb0db3a404be326798db255cfcf75403415 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 14 Apr 2022 17:05:31 -0700 Subject: [PATCH 13/84] thread-safe audienceSegmentsHandler --- DemoSwiftApp/AppDelegate.swift | 1 + DemoSwiftApp/Samples/SamplesForAPI.swift | 18 +++ OptimizelySwiftSDK.xcodeproj/project.pbxproj | 148 +++++++++--------- ...er.swift => AudienceSegmentsHandler.swift} | 17 +- .../AudienceSegments/ZaiusApiManager.swift | 4 +- Sources/Optimizely/OptimizelyClient.swift | 32 ++-- ...ift => AudienceSegmentsHandlerTests.swift} | 4 +- .../OptimizelyUserContextTests_Segments.swift | 11 +- 8 files changed, 137 insertions(+), 98 deletions(-) rename Sources/AudienceSegments/{DefaultAudienceSegmentsHandler.swift => AudienceSegmentsHandler.swift} (82%) rename Tests/OptimizelyTests-Common/{DefaultAudienceSegmentsHandlerTests.swift => AudienceSegmentsHandlerTests.swift} (97%) diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 0a4c2823..bb43e13a 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -135,6 +135,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // 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/Samples/SamplesForAPI.swift b/DemoSwiftApp/Samples/SamplesForAPI.swift index 02be33a1..3cce8410 100644 --- a/DemoSwiftApp/Samples/SamplesForAPI.swift +++ b/DemoSwiftApp/Samples/SamplesForAPI.swift @@ -260,6 +260,24 @@ class SamplesForAPI { } + // MARK: - AudienceSegments + + static func checkAudienceSegments(optimizely: OptimizelyClient) { + // override the default handler if cache size and timeout need to be customized + optimizely.audienceSegmentsHandler = AudienceSegmentsHandler(cacheMaxSize: 12, cacheTimeoutInSecs: 123) + + let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"]) + user.fetchQualifiedSegments(apiKey: "sample-api-key", 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 92f20b20..6abcf1f1 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -641,22 +641,22 @@ 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; 6E6522C9278DF27900954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522DE278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522DF278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E0278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E1278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E2278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E3278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E4278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E5278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E6278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E7278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E8278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522E9278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522EA278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; - 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */; }; + 6E6522DE278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522DF278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E0278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E2278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E3278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E4278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E5278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E6278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E7278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E8278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522EA278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522EB278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522EC278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; + 6E6522ED278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.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 */; }; @@ -1769,12 +1769,28 @@ 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 */; }; + 8405834E2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 8405834F2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583502808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583512808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583522808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583532808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583542808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583552808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583562808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583572808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583582808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 840583592808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 8405835A2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 8405835B2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 8405835C2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; + 8405835D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; 8428D3D02807337400D0FB0C /* LRUCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */; }; 8428D3D12807337400D0FB0C /* LRUCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */; }; 8428D3D5280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; 8428D3D7280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; - 84632F1128049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift */; }; - 84632F1228049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift */; }; + 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */; }; + 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.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 */; }; @@ -1809,22 +1825,6 @@ 84E7ABCA27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */; }; 84F6BAB327FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */; }; 84F6BAB427FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */; }; - 84F6BAB627FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAB727FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAB827FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAB927FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BABA27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BABB27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BABC27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BABD27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BABE27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BABF27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAC027FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAC127FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAC227FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAC327FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAC427FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; - 84F6BAC527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */; }; 84F6BACC27FCFAD7004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; 84F6BAD727FCFB14004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; 84F6BAD827FCFB15004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; @@ -2056,7 +2056,7 @@ 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 = ""; }; 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusApiManager.swift; sourceTree = ""; }; - 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAudienceSegmentsHandler.swift; sourceTree = ""; }; + 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandler.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 = ""; }; @@ -2244,13 +2244,13 @@ 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 = ""; }; + 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPTAudienceSegmentHandler.swift; sourceTree = ""; }; 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCacheTests.swift; sourceTree = ""; }; 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiusApiManagerTests.swift; sourceTree = ""; }; - 84632F1028049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAudienceSegmentsHandlerTests.swift; sourceTree = ""; }; + 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandlerTests.swift; sourceTree = ""; }; 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelySegmentOption.swift; sourceTree = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; - 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPTAudienceSegmentHandler.swift; sourceTree = ""; }; 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_audience_segments.json; sourceTree = ""; }; 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments_Decide.swift; sourceTree = ""; }; BD6485812491474500F30986 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2464,7 +2464,7 @@ 6E6522B8278DF20F00954EA1 /* AudienceSegments */ = { isa = PBXGroup; children = ( - 6E6522DD278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift */, + 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */, 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, 6E6522FF278E688B00954EA1 /* LRUCache.swift */, ); @@ -2662,7 +2662,7 @@ 6E7516A222C520D400B2B157 /* OPTDataStore.swift */, 6E7516A322C520D400B2B157 /* OPTDecisionService.swift */, 6E7516A522C520D400B2B157 /* OPTBucketer.swift */, - 84F6BAB527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift */, + 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */, ); path = Protocols; sourceTree = ""; @@ -2737,6 +2737,7 @@ 6E75197E22C5211100B2B157 /* OptimizelyTests-Common */ = { isa = PBXGroup; children = ( + 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */, 6E75198522C5211100B2B157 /* BatchEventBuilderTest.swift */, 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */, 6E75198722C5211100B2B157 /* BatchEventBuilderTests_Events.swift */, @@ -2755,7 +2756,6 @@ 6E75199122C5211100B2B157 /* DecisionServiceTests_Features.swift */, 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */, 6E75198622C5211100B2B157 /* DecisionServiceTests_UserProfiles.swift */, - 84632F1028049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift */, 6E75198822C5211100B2B157 /* DefaultLoggerTests.swift */, 6E75198922C5211100B2B157 /* DefaultUserProfileServiceTests.swift */, 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */, @@ -3921,7 +3921,7 @@ 6E14CD942423F9A700010234 /* FeatureFlag.swift in Sources */, 6E14CD9D2423F9C300010234 /* OPTDatafileHandler.swift in Sources */, 6E424BE3263228E90081004A /* AtomicArray.swift in Sources */, - 84F6BABD27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583552808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3624BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E14CD7C2423F98D00010234 /* DefaultDatafileHandler.swift in Sources */, 6E14CD9B2423F9C300010234 /* OPTDataStore.swift in Sources */, @@ -3938,7 +3938,7 @@ C78CAFA824486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E14CD932423F9A700010234 /* Experiment.swift in Sources */, 6E14CD982423F9C300010234 /* BackgroundingCallbacks.swift in Sources */, - 6E6522E5278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E5278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E623F07253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5C2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E14CDA52423F9C300010234 /* MurmurHash3.swift in Sources */, @@ -4013,14 +4013,14 @@ 6E424CF926324B620081004A /* DecisionResponse.swift in Sources */, 6E424CFA26324B620081004A /* DataStoreMemory.swift in Sources */, 6E424CFB26324B620081004A /* DataStoreUserDefaults.swift in Sources */, - 6E6522E4278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E4278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */, 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */, 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */, 6E424CFF26324B620081004A /* BatchEvent.swift in Sources */, 6E424D0026324B620081004A /* EventForDispatch.swift in Sources */, 6E424D0126324B620081004A /* SemanticVersion.swift in Sources */, - 84F6BABC27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583542808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E424D0226324B620081004A /* Audience.swift in Sources */, 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */, @@ -4139,7 +4139,7 @@ 6E75172B22C520D400B2B157 /* Constants.swift in Sources */, 6E7516BF22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E424BDE263228E90081004A /* AtomicArray.swift in Sources */, - 84F6BAB727FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 8405834F2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E7517E122C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75178B22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E75177F22C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4164,7 +4164,7 @@ 6E75189522C520D400B2B157 /* Experiment.swift in Sources */, 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84B4D75127E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522DF278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522DF278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E7518D122C520D400B2B157 /* AttributeValue.swift in Sources */, 6E75182922C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75171F22C520D400B2B157 /* OptimizelyResult.swift in Sources */, @@ -4204,7 +4204,7 @@ 6E7518C022C520D400B2B157 /* Variable.swift in Sources */, 6E75181822C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E424BE8263228E90081004A /* AtomicArray.swift in Sources */, - 84F6BAC227FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 8405835A2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3B24BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75185422C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E9B11B422C5489500C22D81 /* OTUtils.swift in Sources */, @@ -4221,7 +4221,7 @@ C78CAFAD24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7516AE22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75195022C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 6E6522EA278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522EA278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E623F0C253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF612445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516D222C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4294,7 +4294,7 @@ 6E75174622C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E75181422C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */, - 84F6BABE27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583562808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E7516C222C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75188022C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E9B11DD22C548A200C22D81 /* OptimizelyClientTests_Valid.swift in Sources */, @@ -4343,7 +4343,7 @@ 6E7517FC22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E75178222C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7518A422C520D400B2B157 /* FeatureFlag.swift in Sources */, - 6E6522E6278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E6278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75185C22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E20050C26B4D28500278087 /* MockLogger.swift in Sources */, 6E75176A22C520D400B2B157 /* Utils.swift in Sources */, @@ -4406,10 +4406,10 @@ 0B97DDA2249D4A25003DE606 /* SemanticVersion.swift in Sources */, 6E7516C522C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75189B22C520D400B2B157 /* Experiment.swift in Sources */, - 84F6BAC127FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583592808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75176122C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75180B22C520D400B2B157 /* DataStoreFile.swift in Sources */, - 6E6522E9278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6EF8DE2324BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7517C322C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190722C520D500B2B157 /* Attribute.swift in Sources */, @@ -4499,7 +4499,6 @@ 6E9B116C22C5487100C22D81 /* BucketTests_ExpToVariation.swift in Sources */, 6E7E9C4C25240D31009E4426 /* OptimizelyUserContextTests_Objc.m in Sources */, 6E75193922C520D500B2B157 /* OPTDataStore.swift in Sources */, - 84F6BAC327FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E7518C122C520D400B2B157 /* Variable.swift in Sources */, 6E75170F22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6EC6DD4C24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, @@ -4522,6 +4521,7 @@ 6E75190922C520D500B2B157 /* Attribute.swift in Sources */, 6E75177B22C520D400B2B157 /* SDKVersion.swift in Sources */, 84E7ABC827D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, + 8405835B2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E7E9B562523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */, 6EC6DD3C24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E75192D22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, @@ -4537,7 +4537,7 @@ 6E7517D122C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6EA2CC2D2345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFAE24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, - 84632F1228049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift in Sources */, + 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */, 6E7517AB22C520D400B2B157 /* Array+Extension.swift in Sources */, 6E75186122C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E75172722C520D400B2B157 /* OptimizelyResult.swift in Sources */, @@ -4581,7 +4581,7 @@ 6E9B116F22C5487100C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3F25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75174B22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 6E6522EB278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522EB278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75187922C520D400B2B157 /* Variation.swift in Sources */, 6E75191522C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75195D22C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -4646,7 +4646,7 @@ 6E86CEAE24FDC84A005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E9B119922C5488300C22D81 /* UserAttributeTests_Evaluate.swift in Sources */, 6E9B11A422C5488300C22D81 /* ProjectTests.swift in Sources */, - 84F6BAC427FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 8405835C2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B119622C5488300C22D81 /* AudienceTests.swift in Sources */, 6E7518B622C520D400B2B157 /* Group.swift in Sources */, 6E7516D422C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4680,7 +4680,7 @@ 6E7518E622C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75179422C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11B722C5489600C22D81 /* MockUrlSession.swift in Sources */, - 6E6522EC278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522EC278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E9B119222C5488300C22D81 /* FeatureVariableTests.swift in Sources */, 6E75176422C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF632445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4728,9 +4728,8 @@ 6E7E9B372523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift in Sources */, 6E9B115622C5486E00C22D81 /* DecisionListenerTests.swift in Sources */, 6E9B114722C5486E00C22D81 /* OptimizelyErrorTests.swift in Sources */, - 84F6BABB27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B114B22C5486E00C22D81 /* BatchEventBuilderTest.swift in Sources */, - 84632F1128049F5C00F06D9C /* DefaultAudienceSegmentsHandlerTests.swift in Sources */, + 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */, 6E9B115A22C5486E00C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B115422C5486E00C22D81 /* LoggerTests.swift in Sources */, 6E7518DF22C520D400B2B157 /* ConditionLeaf.swift in Sources */, @@ -4780,7 +4779,7 @@ 6E75181322C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD6924AE94820017D296 /* OptimizelyUserContextTests.swift in Sources */, 6E75171522C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, - 6E6522E3278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E3278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6EA2CC272345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFA724486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E75185B22C520D400B2B157 /* FeatureVariable.swift in Sources */, @@ -4795,6 +4794,7 @@ 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, 6E9B114F22C5486E00C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E7518F722C520D500B2B157 /* UserAttribute.swift in Sources */, + 840583532808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75174522C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E8A3D492637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E981FC2232C363300FADDD6 /* DecisionListenerTests_Datafile.swift in Sources */, @@ -4892,7 +4892,7 @@ 6E86CEA924FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E9B118322C5488100C22D81 /* UserAttributeTests_Evaluate.swift in Sources */, 6E9B118E22C5488100C22D81 /* ProjectTests.swift in Sources */, - 84F6BABF27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583572808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B118022C5488100C22D81 /* AudienceTests.swift in Sources */, 6E7518B122C520D400B2B157 /* Group.swift in Sources */, 6E7516CF22C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4926,7 +4926,7 @@ 6E7518E122C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75178F22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11AD22C5489300C22D81 /* MockUrlSession.swift in Sources */, - 6E6522E7278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E7278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E9B117C22C5488100C22D81 /* FeatureVariableTests.swift in Sources */, 6E75175F22C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF5E2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4980,7 +4980,7 @@ 6E75181622C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75188E22C520D400B2B157 /* Project.swift in Sources */, 6E994B3C25A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 6E6522E8278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E8278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75189A22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3924ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E75179C22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -5016,7 +5016,7 @@ 6E4544B4270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75184622C520D400B2B157 /* Event.swift in Sources */, 6E7517CE22C520D400B2B157 /* DefaultBucketer.swift in Sources */, - 84F6BAC027FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583582808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75180A22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516B822C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C222C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, @@ -5074,7 +5074,7 @@ 6E75181B22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75189322C520D400B2B157 /* Project.swift in Sources */, 6E994B4125A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 6E6522ED278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522ED278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75189F22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3E24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E7517A122C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -5110,7 +5110,7 @@ 6E4544B9270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75184B22C520D400B2B157 /* Event.swift in Sources */, 6E7517D322C520D400B2B157 /* DefaultBucketer.swift in Sources */, - 84F6BAC527FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 8405835D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75180F22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516BD22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C722C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, @@ -5202,7 +5202,7 @@ 6E75194822C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7518E822C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E424BDD263228E90081004A /* AtomicArray.swift in Sources */, - 84F6BAB627FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 8405834E2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75191822C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E7518B822C520D400B2B157 /* Variable.swift in Sources */, 6E75173622C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5227,7 +5227,7 @@ 6E75192422C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84B4D75027E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522DE278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522DE278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E75193022C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7517E022C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E7516FA22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -5267,7 +5267,7 @@ 6E7518BA22C520D400B2B157 /* Variable.swift in Sources */, 6E75181222C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E424BE1263228E90081004A /* AtomicArray.swift in Sources */, - 84F6BABA27FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583522808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3424BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75184E22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E9B11A822C5489200C22D81 /* OTUtils.swift in Sources */, @@ -5284,7 +5284,7 @@ C78CAFA624486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7518D222C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516A822C520D400B2B157 /* DefaultLogger.swift in Sources */, - 6E6522E2278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E2278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 6E623F05253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5A2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E75194A22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -5357,7 +5357,7 @@ 75C71A0D25E454460084187E /* OptimizelyUserContext+ObjC.swift in Sources */, 75C71A0E25E454460084187E /* DefaultLogger.swift in Sources */, 75C71A0F25E454460084187E /* DefaultUserProfileService.swift in Sources */, - 84F6BAB927FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583512808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 75C71A1025E454460084187E /* DefaultEventDispatcher.swift in Sources */, 75C71A1125E454460084187E /* OPTLogger.swift in Sources */, 75C71A1225E454460084187E /* OPTUserProfileService.swift in Sources */, @@ -5380,7 +5380,7 @@ 75C71A2125E454460084187E /* EventForDispatch.swift in Sources */, 75C71A2225E454460084187E /* SemanticVersion.swift in Sources */, 75C71A2325E454460084187E /* Audience.swift in Sources */, - 6E6522E1278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 75C71A2425E454460084187E /* AttributeValue.swift in Sources */, 75C71A2525E454460084187E /* ConditionLeaf.swift in Sources */, 75C71A2625E454460084187E /* ConditionHolder.swift in Sources */, @@ -5470,7 +5470,7 @@ BD64855E2491474500F30986 /* OPTDatafileHandler.swift in Sources */, BD64855F2491474500F30986 /* ConditionHolder.swift in Sources */, 6E424BDF263228E90081004A /* AtomicArray.swift in Sources */, - 84F6BAB827FCE4CD004BE62A /* OPTAudienceSegmentHandler.swift in Sources */, + 840583502808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, BD6485602491474500F30986 /* OPTNotificationCenter.swift in Sources */, BD6485612491474500F30986 /* Variable.swift in Sources */, BD6485622491474500F30986 /* MurmurHash3.swift in Sources */, @@ -5495,7 +5495,7 @@ BD6485722491474500F30986 /* DataStoreQueueStack.swift in Sources */, BD6485732491474500F30986 /* OptimizelyConfig.swift in Sources */, 84B4D75227E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522E0278E4F3800954EA1 /* DefaultAudienceSegmentsHandler.swift in Sources */, + 6E6522E0278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, BD6485742491474500F30986 /* OPTDataStore.swift in Sources */, BD6485752491474500F30986 /* DefaultDecisionService.swift in Sources */, BD6485762491474500F30986 /* OptimizelyLogLevel.swift in Sources */, diff --git a/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift b/Sources/AudienceSegments/AudienceSegmentsHandler.swift similarity index 82% rename from Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift rename to Sources/AudienceSegments/AudienceSegmentsHandler.swift index c126a1d5..7dcee2a0 100644 --- a/Sources/AudienceSegments/DefaultAudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/AudienceSegmentsHandler.swift @@ -17,16 +17,17 @@ import Foundation import UIKit -public class DefaultAudienceSegmentsHandler: OPTAudienceSegmentsHandler { - // configurable size + timeout - public static var cacheMaxSize = 100 - public static var cacheTimeoutInSecs = 10*60 - +public class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { var zaiusMgr = ZaiusApiManager() - var segmentsCache = LRUCache(size: DefaultAudienceSegmentsHandler.cacheMaxSize, - timeoutInSecs: DefaultAudienceSegmentsHandler.cacheTimeoutInSecs) + var segmentsCache: LRUCache let logger = OPTLoggerFactory.getLogger() + // cache size and timeout can be customized by injecting a subclass + + public init(cacheMaxSize: Int = 100, cacheTimeoutInSecs: Int = 600) { + segmentsCache = LRUCache(size: cacheMaxSize, timeoutInSecs: cacheTimeoutInSecs) + } + func fetchQualifiedSegments(apiKey: String, userKey: String, userValue: String, @@ -67,7 +68,7 @@ public class DefaultAudienceSegmentsHandler: OPTAudienceSegmentsHandler { // MARK: - Utils -extension DefaultAudienceSegmentsHandler { +extension AudienceSegmentsHandler { func cacheKey(_ userKey: String, _ userValue: String) -> String { return userKey + "-$-" + userValue diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index c9fd8fb0..6eb951a2 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -81,7 +81,7 @@ class ZaiusApiManager { segmentsToCheck: [String]?, completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { if userKey != "vuid" { - completionHandler([], .fetchSegmentsFailed("Currently userKeys other than 'vuid' are not supported yet.")) + completionHandler([], .fetchSegmentsFailed("userKeys other than 'vuid' not supported yet")) return } @@ -93,7 +93,7 @@ class ZaiusApiManager { "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state}}}}}" ] guard let httpBody = try? JSONEncoder().encode(body) else { - completionHandler([], .fetchSegmentsFailed("Invalid query.")) + completionHandler([], .fetchSegmentsFailed("invalid query.")) return } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 54723d8d..9ecdac55 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -52,9 +52,24 @@ open class OptimizelyClient: NSObject { // MARK: - Default Services var decisionService: OPTDecisionService! - var audienceSegmentsHandler: OPTAudienceSegmentsHandler? public var notificationCenter: OPTNotificationCenter? + private var atomicAudienceSegmentsHandler = AtomicProperty() + public var audienceSegmentsHandler: AudienceSegmentsHandler { + get { + // instantiated on the first call (not instantiated when it's not used) + guard let handler = atomicAudienceSegmentsHandler.property else { + let newHandler = AudienceSegmentsHandler() + atomicAudienceSegmentsHandler.property = newHandler + return newHandler + } + return handler + } + set { + atomicAudienceSegmentsHandler.property = newValue + } + } + // MARK: - Public interfaces /// OptimizelyClient init @@ -756,17 +771,14 @@ open class OptimizelyClient: NSObject { userValue: String, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - // instantiate on the first request - let handler = audienceSegmentsHandler ?? DefaultAudienceSegmentsHandler() - let segmentsToCheck = config?.allSegments - handler.fetchQualifiedSegments(apiKey: apiKey, - userKey: userKey, - userValue: userValue, - segmentsToCheck: segmentsToCheck, - options: options, - completionHandler: completionHandler) + audienceSegmentsHandler.fetchQualifiedSegments(apiKey: apiKey, + userKey: userKey, + userValue: userValue, + segmentsToCheck: segmentsToCheck, + options: options, + completionHandler: completionHandler) } } diff --git a/Tests/OptimizelyTests-Common/DefaultAudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift similarity index 97% rename from Tests/OptimizelyTests-Common/DefaultAudienceSegmentsHandlerTests.swift rename to Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift index b636d046..6a6709fc 100644 --- a/Tests/OptimizelyTests-Common/DefaultAudienceSegmentsHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift @@ -16,8 +16,8 @@ import XCTest -class DefaultAudienceSegmentsHandlerTests: XCTestCase { - var handler = DefaultAudienceSegmentsHandler() +class AudienceSegmentsHandlerTests: XCTestCase { + var handler = AudienceSegmentsHandler() var options = [OptimizelySegmentOption]() var apiKey = "valid" diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index f89cfd0e..f45f7239 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -97,15 +97,22 @@ class OptimizelyUserContextTests_Segments: XCTestCase { XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!)) } + + func testCustomizeAudienceSegmentsHandler() { + optimizely.audienceSegmentsHandler = AudienceSegmentsHandler(cacheMaxSize: 12, cacheTimeoutInSecs: 123) + + XCTAssertEqual(12, optimizely.audienceSegmentsHandler?.segmentsCache.size) + XCTAssertEqual(123, optimizely.audienceSegmentsHandler?.segmentsCache.timeoutInSecs) + } } // MARK: - MockAudienceSegmentsHandler -class MockAudienceSegmentsHandler: OPTAudienceSegmentsHandler { +class MockAudienceSegmentsHandler: AudienceSegmentsHandler { var segmentsToCheck: [String]? - func fetchQualifiedSegments(apiKey: String, + override func fetchQualifiedSegments(apiKey: String, userKey: String, userValue: String, segmentsToCheck: [String]?, From afd0e83dbb20e9bd91e19fa0476158c60575fafe Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 14 Apr 2022 17:31:39 -0700 Subject: [PATCH 14/84] clean up --- DemoSwiftApp/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index bb43e13a..b7bd5436 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -135,7 +135,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // For sample codes for APIs, see "Samples/SamplesForAPI.swift" //SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely) //SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely) - SamplesForAPI.checkAudienceSegments(optimizely: self.optimizely) + //SamplesForAPI.checkAudienceSegments(optimizely: self.optimizely) } } From 532656ba6a3afb858e17ab76364130fa24d200ad Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 15 Apr 2022 15:13:15 -0700 Subject: [PATCH 15/84] add segmentsToCheck support --- DemoSwiftApp/Samples/SamplesForAPI.swift | 26 ++++++--- .../AudienceSegmentsHandler.swift | 6 +- .../AudienceSegments/ZaiusApiManager.swift | 40 ++++++++++--- .../OptimizelyClient+Extension.swift | 10 +++- .../OptimizelyUserContext.swift | 3 - Sources/Optimizely/OptimizelyClient.swift | 20 +++++-- .../AudienceSegmentsHandlerTests.swift | 2 +- .../DecisionListenerTests.swift | 8 ++- .../OptimizelyUserContextTests_Segments.swift | 11 ++-- .../ZaiusApiManagerTests.swift | 56 ++++++++++++++++++- 10 files changed, 144 insertions(+), 38 deletions(-) diff --git a/DemoSwiftApp/Samples/SamplesForAPI.swift b/DemoSwiftApp/Samples/SamplesForAPI.swift index 3cce8410..fcb3a13c 100644 --- a/DemoSwiftApp/Samples/SamplesForAPI.swift +++ b/DemoSwiftApp/Samples/SamplesForAPI.swift @@ -16,6 +16,7 @@ import Foundation import Optimizely +import UIKit class SamplesForAPI { @@ -264,17 +265,26 @@ class SamplesForAPI { static func checkAudienceSegments(optimizely: OptimizelyClient) { // override the default handler if cache size and timeout need to be customized - optimizely.audienceSegmentsHandler = AudienceSegmentsHandler(cacheMaxSize: 12, cacheTimeoutInSecs: 123) - - let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"]) - user.fetchQualifiedSegments(apiKey: "sample-api-key", options: [.ignoreCache]) { error in - guard error == nil else { - print("[AudienceSegments] \(error!.errorDescription!)") + let optimizely = OptimizelyClient(sdkKey: "FCnSegiEkRry9rhVMroit4", + periodicDownloadInterval: 60, + segmentsCacheSize: 12, + segmentsCacheTimeout: 123) + optimizely.start { result in + if case .failure(let error) = result { + print("[AudienceSegments] SDK initialization failed: \(error)") return } - let decision = user.decide(key: "show_coupon", options: [.includeReasons]) - print("[AudienceSegments] decision: \(decision)") + let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"]) + user.fetchQualifiedSegments(apiKey: "sample-api-key", 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)") + } } } diff --git a/Sources/AudienceSegments/AudienceSegmentsHandler.swift b/Sources/AudienceSegments/AudienceSegmentsHandler.swift index 7dcee2a0..2d1d0b31 100644 --- a/Sources/AudienceSegments/AudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/AudienceSegmentsHandler.swift @@ -17,15 +17,15 @@ import Foundation import UIKit -public class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { +class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { var zaiusMgr = ZaiusApiManager() var segmentsCache: LRUCache let logger = OPTLoggerFactory.getLogger() // cache size and timeout can be customized by injecting a subclass - public init(cacheMaxSize: Int = 100, cacheTimeoutInSecs: Int = 600) { - segmentsCache = LRUCache(size: cacheMaxSize, timeoutInSecs: cacheTimeoutInSecs) + init(cacheSize: Int, cacheTimeoutInSecs: Int) { + segmentsCache = LRUCache(size: cacheSize, timeoutInSecs: cacheTimeoutInSecs) } func fetchQualifiedSegments(apiKey: String, diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 6eb951a2..a2f390ea 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -19,10 +19,17 @@ import Foundation // ODP GraphQL API // - https://api.zaius.com/v3/graphql +// testODPApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" +// testODPUserIdForAudienceSegments = "d66a9d81923d4d2f99d8f64338976322" + /* GraphQL Request +// fetch all segments curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name is_ready state}}}}}"}' https://api.zaius.com/v3/graphql - + +// fetch info for "has_email" segment only +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"]) {edges {node {name is_ready state}}}}}"}' https://api.zaius.com/v3/graphql + query MyQuery { customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { audiences { @@ -85,12 +92,10 @@ class ZaiusApiManager { return } - if (segmentsToCheck?.count ?? 0) > 0 { - self.logger.w("Selective segments fetching is not supported yet.") - } + let subsetFilter = makeSubsetFilter(segments: segmentsToCheck) let body = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state}}}}}" + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences\(subsetFilter) {edges {node {name is_ready state}}}}}" ] guard let httpBody = try? JSONEncoder().encode(body) else { completionHandler([], .fetchSegmentsFailed("invalid query.")) @@ -144,13 +149,31 @@ class ZaiusApiManager { return URLSession(configuration: .ephemeral) } + func makeSubsetFilter(segments: [String]?) -> String { + // segments = nil: (fetch all segments) + // --> subsetFilter = "" + // segments = []: (fetch none) + // --> subsetFilter = "(subset:[])" + // segments = ["a"]: (fetch one segment) + // --> subsetFilter = "(subset:[\"a\"])" + + var subsetFilter = "" + + if let segments = segments { + let serial = segments.map { "\"\($0)\""}.joined(separator: ",") + subsetFilter = "(subset:[\(serial)])" + } + + return subsetFilter + } + } struct ODPAudience: Decodable { let name: String let isReady: Bool let state: String - let description: String + let description: String? // optional so we can add for debugging var isQualified: Bool { isReady && state == "qualified" @@ -160,13 +183,12 @@ struct ODPAudience: Decodable { guard let dict = dict, let name = dict["name"] as? String, let isReady = dict["is_ready"] as? Bool, - let state = dict["state"] as? String, - let description = dict["description"] as? String else { return nil } + let state = dict["state"] as? String else { return nil } self.name = name self.isReady = isReady self.state = state - self.description = description + self.description = dict["description"] as? String } } diff --git a/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index e8be66c2..e840e9c3 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -56,6 +56,8 @@ extension OptimizelyClient { /// Set this to 0 to disable periodic downloading. /// - defaultLogLevel: default log level (optional. default = .info) /// - defaultDecisionOptions: default decision optiopns (optional) + /// - segmentsCacheSize: maximum size (default = 100) of audience segments cache (optional) + /// - segmentsCacheTimeout: timeout in seconds (default = 600) of audience segments cache (optional) public convenience init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, @@ -63,7 +65,9 @@ extension OptimizelyClient { userProfileService: OPTUserProfileService? = nil, periodicDownloadInterval: Int?, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil) { + defaultDecideOptions: [OptimizelyDecideOption]? = nil, + segmentsCacheSize: Int? = nil, + segmentsCacheTimeout: Int? = nil) { self.init(sdkKey: sdkKey, logger: logger, @@ -71,7 +75,9 @@ extension OptimizelyClient { datafileHandler: datafileHandler, userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions) + defaultDecideOptions: defaultDecideOptions, + segmentsCacheSize: segmentsCacheSize, + segmentsCacheTimeout: segmentsCacheTimeout) let interval = periodicDownloadInterval ?? 10 * 60 if interval > 0 { diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index e245a5f8..156f4b4b 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -159,9 +159,6 @@ public class OptimizelyUserContext { // MARK: - AudienceSegments -let testApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" -let testUserIdForAudienceSegments = "d66a9d81923d4d2f99d8f64338976322" - extension OptimizelyUserContext { /// Fetch all qualified segments for the given user identifier (**userKey** and **userValue**). diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 9ecdac55..45999556 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -54,12 +54,14 @@ open class OptimizelyClient: NSObject { var decisionService: OPTDecisionService! public var notificationCenter: OPTNotificationCenter? + var segmentsCacheSize = 100 + var segmentsCacheTimeoutInSecs = 600 private var atomicAudienceSegmentsHandler = AtomicProperty() - public var audienceSegmentsHandler: AudienceSegmentsHandler { + var audienceSegmentsHandler: AudienceSegmentsHandler { get { // instantiated on the first call (not instantiated when it's not used) guard let handler = atomicAudienceSegmentsHandler.property else { - let newHandler = AudienceSegmentsHandler() + let newHandler = AudienceSegmentsHandler(cacheSize: segmentsCacheSize, cacheTimeoutInSecs: segmentsCacheTimeoutInSecs) atomicAudienceSegmentsHandler.property = newHandler return newHandler } @@ -81,17 +83,27 @@ 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) + /// - segmentsCacheSize: maximum size (default = 100) of audience segments cache (optional) + /// - segmentsCacheTimeout: timeout in seconds (default = 600) of audience segments cache (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, + segmentsCacheSize: Int? = nil, + segmentsCacheTimeout: Int? = nil) { self.sdkKey = sdkKey self.defaultDecideOptions = defaultDecideOptions ?? [] + if let segmentsCacheSize = segmentsCacheSize { + self.segmentsCacheSize = segmentsCacheSize + } + if let segmentsCacheTimeout = segmentsCacheTimeout { + self.segmentsCacheTimeoutInSecs = segmentsCacheTimeout + } super.init() diff --git a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift index 6a6709fc..b7fbf3e4 100644 --- a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift @@ -17,7 +17,7 @@ import XCTest class AudienceSegmentsHandlerTests: XCTestCase { - var handler = AudienceSegmentsHandler() + var handler = AudienceSegmentsHandler(cacheSize: 100, cacheTimeoutInSecs: 100) var options = [OptimizelySegmentOption]() var apiKey = "valid" diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index dc0b7a28..c7ab7171 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift @@ -1213,7 +1213,9 @@ class FakeManager: OptimizelyClient { datafileHandler: OPTDatafileHandler? = nil, userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, - defaultDecideOptions: [OptimizelyDecideOption]? = nil) { + defaultDecideOptions: [OptimizelyDecideOption]? = nil, + segmentsCacheSize: Int? = nil, + segmentsCacheTimeout: Int? = nil) { // clear shared handlers HandlerRegistryService.shared.removeAll() @@ -1224,7 +1226,9 @@ class FakeManager: OptimizelyClient { datafileHandler: datafileHandler, userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions) + defaultDecideOptions: defaultDecideOptions, + segmentsCacheSize: segmentsCacheSize, + segmentsCacheTimeout: segmentsCacheTimeout) let userProfileService = userProfileService ?? DefaultUserProfileService() self.decisionService = FakeDecisionService(userProfileService: userProfileService) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index f45f7239..21412146 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -19,7 +19,7 @@ import XCTest class OptimizelyUserContextTests_Segments: XCTestCase { var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - var segmentHandler = MockAudienceSegmentsHandler() + var segmentHandler = MockAudienceSegmentsHandler(cacheSize: 100, cacheTimeoutInSecs: 100) var user: OptimizelyUserContext! let kApiKey = "any-key" let kUserId = "tester" @@ -99,10 +99,13 @@ class OptimizelyUserContextTests_Segments: XCTestCase { } func testCustomizeAudienceSegmentsHandler() { - optimizely.audienceSegmentsHandler = AudienceSegmentsHandler(cacheMaxSize: 12, cacheTimeoutInSecs: 123) + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, + periodicDownloadInterval: 60, + segmentsCacheSize: 12, + segmentsCacheTimeout: 123) - XCTAssertEqual(12, optimizely.audienceSegmentsHandler?.segmentsCache.size) - XCTAssertEqual(123, optimizely.audienceSegmentsHandler?.segmentsCache.timeoutInSecs) + XCTAssertEqual(12, optimizely.audienceSegmentsHandler.segmentsCache.size) + XCTAssertEqual(123, optimizely.audienceSegmentsHandler.segmentsCache.timeoutInSecs) } } diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift index 31548fe2..6244db8d 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -81,8 +81,8 @@ class ZaiusApiManagerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - func testGraphQLRequest() { - let manager = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) + func testGraphQLRequest_allSegments() { + let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) let sem = DispatchSemaphore(value: 0) manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, error in @@ -105,6 +105,36 @@ class ZaiusApiManagerTests: XCTestCase { XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) } + func testGraphQLRequest_subsetSegments() { + let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) + + let sem = DispatchSemaphore(value: 0) + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: ["a", "b"]) { _, error in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + guard let request = ZaiusApiManagerTests.createdApiRequest else { + XCTFail() + return + } + + let expectedBody = [ + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences(subset:[\"a\",\"b\"]) {edges {node {name is_ready state}}}}}" + ] + + XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) + } + + func testMakeSubsetFilter() { + let manager = ZaiusApiManager() + + XCTAssertEqual("", manager.makeSubsetFilter(segments: nil)) + XCTAssertEqual("(subset:[])", manager.makeSubsetFilter(segments: [])) + XCTAssertEqual("(subset:[\"a\"])", manager.makeSubsetFilter(segments: ["a"])) + XCTAssertEqual("(subset:[\"a\",\"b\",\"c\"])",manager.makeSubsetFilter(segments: ["a", "b", "c"])) + } + func testExtractComponent() { let dict = ["a": ["b": ["c": "v"]]] XCTAssertEqual(["b": ["c": "v"]], dict.extractComponent(keyPath: "a")) @@ -114,6 +144,28 @@ class ZaiusApiManagerTests: XCTestCase { XCTAssertNil(dict.extractComponent(keyPath: "d")) } + // MARK: - Tests with real ODP server + + // TODO: this test can be flaky. replace it with a good test account or remove it later. + + func testLiveODPGraphQL() { + let testODPApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" + let testODPUserIdForAudienceSegments = "d66a9d81923d4d2f99d8f64338976322" + + let manager = ZaiusApiManager() + + let sem = DispatchSemaphore(value: 0) + manager.fetch(apiKey: testODPApiKeyForAudienceSegments, + userKey: "vuid", + userValue: testODPUserIdForAudienceSegments, + segmentsToCheck: ["has_email", "has_email_opted_in", "invalid-segment"]) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(["has_email", "has_email_opted_in"], Array(Set(segments!))) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + // MARK: - MockZaiusApiManager class MockZaiusApiManager: ZaiusApiManager { From 6a4ef83bacf8f3a527ca8acb4d78fdf3627a4158 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 15 Apr 2022 15:44:26 -0700 Subject: [PATCH 16/84] add useSubset option --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 2 +- .../OptimizelySegmentOption.swift | 2 ++ Sources/Optimizely/OptimizelyClient.swift | 3 +- .../AudienceSegmentsHandlerTests.swift | 29 ++++++++++--------- .../OptimizelyUserContextTests_Segments.swift | 27 +++++++++++++++-- 5 files changed, 46 insertions(+), 17 deletions(-) rename Sources/{Optimizely+Decide => AudienceSegments}/OptimizelySegmentOption.swift (88%) diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 6abcf1f1..e7981ca2 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -2467,6 +2467,7 @@ 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */, 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, 6E6522FF278E688B00954EA1 /* LRUCache.swift */, + 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, ); path = AudienceSegments; sourceTree = ""; @@ -2879,7 +2880,6 @@ 6EC6DD4024ABF89B0017D296 /* OptimizelyUserContext.swift */, 6EF8DE0A24BD1BB1008B9488 /* OptimizelyDecision.swift */, 6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */, - 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, 6E86CEA124FDC836005DAFED /* OptimizelyUserContext+ObjC.swift */, ); path = "Optimizely+Decide"; diff --git a/Sources/Optimizely+Decide/OptimizelySegmentOption.swift b/Sources/AudienceSegments/OptimizelySegmentOption.swift similarity index 88% rename from Sources/Optimizely+Decide/OptimizelySegmentOption.swift rename to Sources/AudienceSegments/OptimizelySegmentOption.swift index dd186ca1..12e12712 100644 --- a/Sources/Optimizely+Decide/OptimizelySegmentOption.swift +++ b/Sources/AudienceSegments/OptimizelySegmentOption.swift @@ -18,6 +18,8 @@ import Foundation /// Options controlling audience segments. @objc public enum OptimizelySegmentOption: Int { + // fetch subset segments only (used in the project). default is fetch-all-segments + case useSubset // ignore cache (save/lookup) case ignoreCache // reset cache diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 45999556..58c36c5f 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -783,7 +783,8 @@ open class OptimizelyClient: NSObject { userValue: String, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - let segmentsToCheck = config?.allSegments + + let segmentsToCheck = options.contains(.useSubset) ? config?.allSegments : nil audienceSegmentsHandler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, diff --git a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift index b7fbf3e4..fda654d2 100644 --- a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift @@ -58,6 +58,22 @@ class AudienceSegmentsHandlerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } + func testError() { + let sem = DispatchSemaphore(value: 0) + + handler.fetchQualifiedSegments(apiKey: "invalid-key", userKey: userKey, userValue: userValue, + segmentsToCheck: nil, options: []) { segments, error in + XCTAssertNotNil(error) + XCTAssert(segments!.isEmpty ) + sem.signal() + } + + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + // MARK: - OptimizelySegmentOption + // NOTE: The "useSubset" option is fully tested in "OptimizelyUserContextTests_Segments", so skip here. + func testOptions_ignoreCache() { setCache(userKey, userValue, ["a"]) options = [.ignoreCache] @@ -95,19 +111,6 @@ class AudienceSegmentsHandlerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } - func testError() { - let sem = DispatchSemaphore(value: 0) - - handler.fetchQualifiedSegments(apiKey: "invalid-key", userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: []) { segments, error in - XCTAssertNotNil(error) - XCTAssert(segments!.isEmpty ) - sem.signal() - } - - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) - } - // MARK: - Utils func setCache(_ userKey: String, _ userValue: String, _ value: [String]) { diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index 21412146..61a5f626 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -83,14 +83,37 @@ class OptimizelyUserContextTests_Segments: XCTestCase { XCTAssertNil(segmentHandler.segmentsToCheck) } - func testFetchQualifiedSegments_segmentsToCheck_validAfterStart() { + func testFetchQualifiedSegments_segmentsToCheck_emptyAfterStart() { + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + try? optimizely.start(datafile: datafile) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: kApiKey) { error in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + + XCTAssertNil(segmentHandler.segmentsToCheck) + } + + func testFetchQualifiedSegments_segmentsToCheck_emptyBeforeStart_withUseSubsetOption() { + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: kApiKey, options: [.useSubset]) { error in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + + XCTAssertNil(segmentHandler.segmentsToCheck) + } + + func testFetchQualifiedSegments_segmentsToCheck_validAfterStart_withUseSubsetOption() { let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! try? optimizely.start(datafile: datafile) // fetch segments after SDK initialized, so segmentsToCheck will be used. let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { error in + user.fetchQualifiedSegments(apiKey: kApiKey, options: [.useSubset]) { error in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) From bb541f7abac0951a443266bf41d3cf03b98265f1 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 18 Apr 2022 14:21:56 -0700 Subject: [PATCH 17/84] add performance test for user context --- .../OptimizelyUserContextTests.swift | 63 +++++++++++++++++++ .../ZaiusApiManagerTests.swift | 2 +- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift index c50f33aa..97ed78a9 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift @@ -205,3 +205,66 @@ class OptimizelyUserContextTests: XCTestCase { } } + +// MARK: - Performance + +extension OptimizelyUserContextTests { + + func testPerformance_create() { + let timeInMicrosecs = measureTime { + _ = expOptimizely.createUserContext(userId: "tester", attributes: ["a1": "b1"]) + } + + XCTAssert(timeInMicrosecs < 10, "user context create takes too long (\(timeInMicrosecs) microsecs)") + } + + func testPerformance_clone() { + let user = OptimizelyUserContext(optimizely: expOptimizely, 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: expOptimizely, 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)") + } + +} + +// MARK: - Utils + +extension OptimizelyUserContextTests { + + func measureTime(operation: () -> Void) -> Double { + let measureCount = 10000 + + let startTime = CFAbsoluteTimeGetCurrent() + for _ in 0.. Date: Mon, 18 Apr 2022 15:51:11 -0700 Subject: [PATCH 18/84] clean up user context --- Sources/Optimizely+Decide/OptimizelyUserContext.swift | 10 ++++++---- Sources/Utils/AtomicDictionary.swift | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 156f4b4b..5f19f062 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -21,14 +21,14 @@ 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? - var atomicQualifiedSegments: AtomicArray? + private var atomicQualifiedSegments: AtomicArray? public var qualifiedSegments: [String]? { get { return atomicQualifiedSegments?.property @@ -222,7 +222,7 @@ public struct OptimizelyDecisionContext: Hashable { } /// Forced Decision -public struct OptimizelyForcedDecision { +public struct OptimizelyForcedDecision: Equatable { public let variationKey: String public init(variationKey: String) { @@ -291,7 +291,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?.property == rhs.forcedDecisions?.property && + lhs.qualifiedSegments == rhs.qualifiedSegments } } 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] { From 6d0ea43d92db5f4f78638c552792dba1311d5192 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 18 Apr 2022 17:59:54 -0700 Subject: [PATCH 19/84] clean up user context synchronizations --- .../OptimizelyUserContext.swift | 61 ++++++++++--------- Sources/Utils/AtomicProperty.swift | 23 +++---- .../OptimizelyUserContextTests.swift | 10 +++ .../ZaiusApiManagerTests.swift | 2 +- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 5f19f062..5d545c47 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -21,24 +21,25 @@ public class OptimizelyUserContext { weak var optimizely: OptimizelyClient? public var userId: String + private let lock = DispatchQueue(label: "user-context") + 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: AtomicArray? + private var atomicQualifiedSegments: AtomicProperty<[String]> public var qualifiedSegments: [String]? { get { - return atomicQualifiedSegments?.property + return atomicQualifiedSegments.property } set { - if let value = newValue { - atomicQualifiedSegments = AtomicArray(value) - } else { - atomicQualifiedSegments = nil - } + atomicQualifiedSegments.property = newValue } } @@ -46,12 +47,13 @@ public class OptimizelyUserContext { guard let optimizely = self.optimizely else { return nil } let userContext = OptimizelyUserContext(optimizely: optimizely, userId: userId, attributes: attributes) + if let fds = forcedDecisions { - userContext.forcedDecisions = AtomicDictionary(fds.property) + userContext.atomicForcedDecisions = AtomicProperty(property: fds, lock: lock) } - if let qs = atomicQualifiedSegments { - userContext.atomicQualifiedSegments = AtomicArray(qs.property) + if let qs = qualifiedSegments { + userContext.atomicQualifiedSegments = AtomicProperty(property: qs, lock: lock) } return userContext @@ -70,7 +72,9 @@ public class OptimizelyUserContext { attributes: [String: Any?]? = nil) { self.optimizely = optimizely self.userId = userId - self.atomicAttributes = AtomicProperty(property: attributes ?? [:]) + self.atomicAttributes = AtomicProperty(property: attributes ?? [:], lock: lock) + self.atomicForcedDecisions = AtomicProperty(property: nil, lock: lock) + self.atomicQualifiedSegments = AtomicProperty(property: nil, lock: lock) } /// Sets an attribute for a given key. @@ -196,14 +200,14 @@ extension OptimizelyUserContext { return } - self.atomicQualifiedSegments = AtomicArray(segments) + self.atomicQualifiedSegments.property = segments completionHandler(nil) } } // true if the user is qualified for the given segment name public func isQualifiedFor(segment: String) -> Bool { - return atomicQualifiedSegments?.contains(segment) ?? false + return atomicQualifiedSegments.property?.contains(segment) ?? false } } @@ -241,10 +245,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 } @@ -253,9 +260,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. @@ -263,23 +268,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 } @@ -292,7 +293,7 @@ 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.forcedDecisions?.property == rhs.forcedDecisions?.property && + lhs.forcedDecisions == rhs.forcedDecisions && lhs.qualifiedSegments == rhs.qualifiedSegments } diff --git a/Sources/Utils/AtomicProperty.swift b/Sources/Utils/AtomicProperty.swift index acd29469..ed5c0262 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 clzzName = String(describing: T.self) + name += clzzName + 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/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift index 97ed78a9..9cf58868 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift @@ -246,6 +246,16 @@ extension OptimizelyUserContextTests { XCTAssert(timeInMicrosecs < 10, "user context cloning takes too long (\(timeInMicrosecs) microsecs)") } + func testPerformance_decideInvalid() { + let user = OptimizelyUserContext(optimizely: expOptimizely, userId: "tester", attributes: ["a1": "b1"]) + + let timeInMicrosecs = measureTime { + _ = user.decide(key: "invalid") + } + + XCTAssert(timeInMicrosecs < 10, "user context decide-with-invalid takes too long (\(timeInMicrosecs) microsecs)") + } + } // MARK: - Utils diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift index 79806853..a174524d 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -163,7 +163,7 @@ class ZaiusApiManagerTests: XCTestCase { XCTAssertEqual(Set(["has_email", "has_email_opted_in"]), Set(segments!)) sem.signal() } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } // MARK: - MockZaiusApiManager From 12cd3a85f7d5dd0af9657232c8e4723428ae720c Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 19 Apr 2022 10:20:31 -0700 Subject: [PATCH 20/84] add user context performance tests --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 6 + .../OptimizelyUserContextTests.swift | 73 ----------- ...timizelyUserContextTests_Performance.swift | 115 ++++++++++++++++++ 3 files changed, 121 insertions(+), 73 deletions(-) create mode 100644 Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Performance.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index e7981ca2..74eb49d9 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1791,6 +1791,8 @@ 8428D3D7280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */; }; 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.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 */; }; @@ -2248,6 +2250,7 @@ 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCacheTests.swift; sourceTree = ""; }; 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiusApiManagerTests.swift; sourceTree = ""; }; 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandlerTests.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 = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; @@ -2738,6 +2741,7 @@ 6E75197E22C5211100B2B157 /* OptimizelyTests-Common */ = { isa = PBXGroup; children = ( + 84958C5D280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift */, 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */, 6E75198522C5211100B2B157 /* BatchEventBuilderTest.swift */, 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */, @@ -4492,6 +4496,7 @@ 6E75179F22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7516BB22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E424C08263228FD0081004A /* AtomicDictionary.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 */, @@ -4745,6 +4750,7 @@ 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 */, 6EC6DD4524ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift index 9cf58868..c50f33aa 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests.swift @@ -205,76 +205,3 @@ class OptimizelyUserContextTests: XCTestCase { } } - -// MARK: - Performance - -extension OptimizelyUserContextTests { - - func testPerformance_create() { - let timeInMicrosecs = measureTime { - _ = expOptimizely.createUserContext(userId: "tester", attributes: ["a1": "b1"]) - } - - XCTAssert(timeInMicrosecs < 10, "user context create takes too long (\(timeInMicrosecs) microsecs)") - } - - func testPerformance_clone() { - let user = OptimizelyUserContext(optimizely: expOptimizely, 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: expOptimizely, 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: expOptimizely, userId: "tester", attributes: ["a1": "b1"]) - - let timeInMicrosecs = measureTime { - _ = user.decide(key: "invalid") - } - - XCTAssert(timeInMicrosecs < 10, "user context decide-with-invalid takes too long (\(timeInMicrosecs) microsecs)") - } - -} - -// MARK: - Utils - -extension OptimizelyUserContextTests { - - func measureTime(operation: () -> Void) -> Double { - let measureCount = 10000 - - let startTime = CFAbsoluteTimeGetCurrent() - for _ in 0.. Void) -> Double { + let measureCount = 10000 + + let startTime = CFAbsoluteTimeGetCurrent() + for _ in 0.. Date: Tue, 19 Apr 2022 11:58:46 -0700 Subject: [PATCH 21/84] fix fetch api to return segments in completion handler --- .../AudienceSegments/ZaiusApiManager.swift | 10 +++++----- .../OptimizelyUserContext.swift | 10 +++++----- .../OptimizelyUserContextTests_Segments.swift | 19 +++++++++++-------- .../ZaiusApiManagerTests.swift | 10 ++++++---- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index a2f390ea..ecb123e3 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -88,7 +88,7 @@ class ZaiusApiManager { segmentsToCheck: [String]?, completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { if userKey != "vuid" { - completionHandler([], .fetchSegmentsFailed("userKeys other than 'vuid' not supported yet")) + completionHandler(nil, .fetchSegmentsFailed("userKeys other than 'vuid' not supported yet")) return } @@ -98,7 +98,7 @@ class ZaiusApiManager { "query": "query {customer(\(userKey): \"\(userValue)\") {audiences\(subsetFilter) {edges {node {name is_ready state}}}}}" ] guard let httpBody = try? JSONEncoder().encode(body) else { - completionHandler([], .fetchSegmentsFailed("invalid query.")) + completionHandler(nil, .fetchSegmentsFailed("invalid query.")) return } @@ -118,12 +118,12 @@ class ZaiusApiManager { self.logger.d { "GraphQL download failed: \(error)" } - completionHandler([], .fetchSegmentsFailed("download failed")) + completionHandler(nil, .fetchSegmentsFailed("download failed")) return } guard let data = data else { - completionHandler([], .fetchSegmentsFailed("response data empty")) + completionHandler(nil, .fetchSegmentsFailed("response data empty")) return } @@ -133,7 +133,7 @@ class ZaiusApiManager { self.logger.d { "GraphQL decode failed: " + String(bytes: data, encoding: .utf8)! } - completionHandler([], .fetchSegmentsFailed("decode error")) + completionHandler(nil, .fetchSegmentsFailed("decode error")) return } diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 5d545c47..1d4e0e66 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -175,14 +175,14 @@ extension OptimizelyUserContext { /// - userKey: The name of the user identifier (optional). /// - userValue: The value of the user identifier (optional). /// - options: A set of options for fetching qualified segments (optional). - /// - completionHandler: A completion handler to be called with the fetch status success/failure. + /// - completionHandler: A completion handler to be called with the fetch status success/failure. 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(apiKey: String, userKey: String? = nil, userValue: String? = nil, options: [OptimizelySegmentOption] = [], - completionHandler: @escaping (OptimizelyError?) -> Void) { + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { guard let optimizely = self.optimizely else { - completionHandler(.sdkNotReady) + completionHandler(nil, .sdkNotReady) return } @@ -196,12 +196,12 @@ extension OptimizelyUserContext { guard err == nil, let segments = segments else { let error = err ?? OptimizelyError.fetchSegmentsFailed("invalid segments") self.logger.e(error) - completionHandler(error) + completionHandler(nil, error) return } self.atomicQualifiedSegments.property = segments - completionHandler(nil) + completionHandler(segments, nil) } } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index 61a5f626..ff1e4d79 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -43,9 +43,10 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_successDefaultUser() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { error in + user.fetchQualifiedSegments(apiKey: kApiKey) { segments, error in XCTAssertNil(error) - XCTAssert(self.user.qualifiedSegments == [self.kApiKey, self.kUserIdKey, self.kUserId]) + XCTAssert(segments == [self.kApiKey, self.kUserIdKey, self.kUserId]) + XCTAssert(self.user.qualifiedSegments == segments) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -55,8 +56,9 @@ class OptimizelyUserContextTests_Segments: XCTestCase { user.optimizely = nil let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { error in + user.fetchQualifiedSegments(apiKey: kApiKey) { segments, error in XCTAssertEqual(OptimizelyError.sdkNotReady.reason, error?.reason) + XCTAssertNil(segments) XCTAssertNil(self.user.qualifiedSegments) sem.signal() } @@ -65,8 +67,9 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_fetchFailed() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: "invalid-key") { error in + user.fetchQualifiedSegments(apiKey: "invalid-key") { segments, error in XCTAssertNotNil(error) + XCTAssertNil(segments) XCTAssertNil(self.user.qualifiedSegments) sem.signal() } @@ -75,7 +78,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_segmentsToCheck_emptyBeforeStart() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { error in + user.fetchQualifiedSegments(apiKey: kApiKey) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -88,7 +91,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { try? optimizely.start(datafile: datafile) let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { error in + user.fetchQualifiedSegments(apiKey: kApiKey) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -98,7 +101,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_segmentsToCheck_emptyBeforeStart_withUseSubsetOption() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, options: [.useSubset]) { error in + user.fetchQualifiedSegments(apiKey: kApiKey, options: [.useSubset]) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -113,7 +116,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { // fetch segments after SDK initialized, so segmentsToCheck will be used. let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, options: [.useSubset]) { error in + user.fetchQualifiedSegments(apiKey: kApiKey, options: [.useSubset]) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift index a174524d..23efd20e 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -55,12 +55,13 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, error in + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { 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))) @@ -70,12 +71,13 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, error in + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in if case .fetchSegmentsFailed("download failed") = error { XCTAssert(true) } else { XCTFail() } + XCTAssertNil(segments) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) @@ -85,7 +87,7 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, error in + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) @@ -109,7 +111,7 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: ["a", "b"]) { _, error in + manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: ["a", "b"]) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) From 71e6fa67b5bfc518cbea9e687006c4041e013735 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 19 Apr 2022 13:24:11 -0700 Subject: [PATCH 22/84] clean up --- .../Optimizely+Decide/OptimizelyUserContext.swift | 13 +++++++------ Sources/Optimizely/OptimizelyClient.swift | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 1d4e0e66..f6f9273e 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -20,9 +20,7 @@ import Foundation public class OptimizelyUserContext { weak var optimizely: OptimizelyClient? public var userId: String - - private let lock = DispatchQueue(label: "user-context") - + private var atomicAttributes: AtomicProperty<[String: Any?]> public var attributes: [String: Any?] { return atomicAttributes.property ?? [:] @@ -38,6 +36,7 @@ public class OptimizelyUserContext { get { return atomicQualifiedSegments.property } + // keep this public set api for clients to set directly (testing/debugging) set { atomicQualifiedSegments.property = newValue } @@ -49,11 +48,11 @@ public class OptimizelyUserContext { let userContext = OptimizelyUserContext(optimizely: optimizely, userId: userId, attributes: attributes) if let fds = forcedDecisions { - userContext.atomicForcedDecisions = AtomicProperty(property: fds, lock: lock) + userContext.atomicForcedDecisions.property = fds } if let qs = qualifiedSegments { - userContext.atomicQualifiedSegments = AtomicProperty(property: qs, lock: lock) + userContext.atomicQualifiedSegments.property = qs } return userContext @@ -72,6 +71,8 @@ public class OptimizelyUserContext { attributes: [String: Any?]? = nil) { self.optimizely = optimizely self.userId = userId + + 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) @@ -175,7 +176,7 @@ extension OptimizelyUserContext { /// - userKey: The name of the user identifier (optional). /// - userValue: The value of the user identifier (optional). /// - options: A set of options for fetching qualified segments (optional). - /// - completionHandler: A completion handler to be called with the fetch status success/failure. 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. + /// - 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(apiKey: String, userKey: String? = nil, userValue: String? = nil, diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 58c36c5f..77ce9b16 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -61,9 +61,9 @@ open class OptimizelyClient: NSObject { get { // instantiated on the first call (not instantiated when it's not used) guard let handler = atomicAudienceSegmentsHandler.property else { - let newHandler = AudienceSegmentsHandler(cacheSize: segmentsCacheSize, cacheTimeoutInSecs: segmentsCacheTimeoutInSecs) - atomicAudienceSegmentsHandler.property = newHandler - return newHandler + let defaultHandler = AudienceSegmentsHandler(cacheSize: segmentsCacheSize, cacheTimeoutInSecs: segmentsCacheTimeoutInSecs) + atomicAudienceSegmentsHandler.property = defaultHandler + return defaultHandler } return handler } From b339b661c86a1c5df02585a9dae5b34a5d9e2d78 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 19 Apr 2022 15:40:02 -0700 Subject: [PATCH 23/84] fix for old swift in ci --- .../AudienceSegments/AudienceSegmentsHandler.swift | 4 ++-- Sources/Data Model/Audience/UserAttribute.swift | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/AudienceSegments/AudienceSegmentsHandler.swift b/Sources/AudienceSegments/AudienceSegmentsHandler.swift index 2d1d0b31..062ebe96 100644 --- a/Sources/AudienceSegments/AudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/AudienceSegmentsHandler.swift @@ -34,7 +34,7 @@ class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { segmentsToCheck: [String]? = nil, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - let cacheKey = cacheKey(userKey, userValue) + let cacheKey = makeCacheKey(userKey, userValue) let ignoreCache = options.contains(.ignoreCache) let resetCache = options.contains(.resetCache) @@ -70,7 +70,7 @@ class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { extension AudienceSegmentsHandler { - func cacheKey(_ userKey: String, _ userValue: String) -> String { + func makeCacheKey(_ userKey: String, _ userValue: String) -> String { return userKey + "-$-" + userValue } diff --git a/Sources/Data Model/Audience/UserAttribute.swift b/Sources/Data Model/Audience/UserAttribute.swift index 7a6beb34..bbb1d476 100644 --- a/Sources/Data Model/Audience/UserAttribute.swift +++ b/Sources/Data Model/Audience/UserAttribute.swift @@ -117,14 +117,14 @@ extension UserAttribute { } let attributes = user.attributes - let rawAttributeValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'" + let rawValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'" if matchFinal == .exists { - return !(rawAttributeValue is NSNull || rawAttributeValue == nil) + return !(rawValue is NSNull || rawValue == nil) } // all other matches requires valid value - + guard let value = value else { throw OptimizelyError.userAttributeNilValue(stringRepresentation) } @@ -138,11 +138,13 @@ extension UserAttribute { 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 = rawAttributeValue else { + + guard let rawAttributeValue = rawValue else { throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal) } From 87e713041ff8e3049d94034b18b37b97f68c2c77 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 19 Apr 2022 16:08:19 -0700 Subject: [PATCH 24/84] clean up --- Sources/AudienceSegments/ZaiusApiManager.swift | 2 +- .../OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index ecb123e3..7daa0aec 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -176,7 +176,7 @@ struct ODPAudience: Decodable { let description: String? // optional so we can add for debugging var isQualified: Bool { - isReady && state == "qualified" + return isReady && (state == "qualified") } init?(_ dict: [String: Any]?) { diff --git a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift index fda654d2..04fe03af 100644 --- a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift @@ -114,12 +114,12 @@ class AudienceSegmentsHandlerTests: XCTestCase { // MARK: - Utils func setCache(_ userKey: String, _ userValue: String, _ value: [String]) { - let cacheKey = handler.cacheKey(userKey, userValue) + let cacheKey = handler.makeCacheKey(userKey, userValue) handler.segmentsCache.save(key: cacheKey, value: value) } func peekCache(_ userKey: String, _ userValue: String) -> [String]? { - let cacheKey = handler.cacheKey(userKey, userValue) + let cacheKey = handler.makeCacheKey(userKey, userValue) return handler.segmentsCache.peek(key: cacheKey) } From be79815e5257b462425077c0ab125a993ac45134 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 20 Apr 2022 09:34:32 -0700 Subject: [PATCH 25/84] skip debug-mode tests in CI --- Tests/OptimizelyTests-Common/LRUCacheTests.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/OptimizelyTests-Common/LRUCacheTests.swift b/Tests/OptimizelyTests-Common/LRUCacheTests.swift index 13337826..d08cfc10 100644 --- a/Tests/OptimizelyTests-Common/LRUCacheTests.swift +++ b/Tests/OptimizelyTests-Common/LRUCacheTests.swift @@ -24,10 +24,24 @@ class LRUCacheTests: XCTestCase { XCTAssertEqual(2000, cache.timeoutInSecs) cache = LRUCache(size: 0, timeoutInSecs: 0) + + #if DEBUG XCTAssertEqual(1, cache.size) XCTAssertEqual(1, cache.timeoutInSecs) + #else + XCTAssertEqual(10, cache.size) + XCTAssertEqual(60, cache.timeoutInSecs) + #endif } +} + +// tests below will be skipped in CI (travis/actions) since they use time control and debug-mode configs. + +#if DEBUG + +extension LRUCacheTests { + func testSaveAndLookup() { let maxSize = 2 let cache = LRUCache(size: maxSize, timeoutInSecs: 1000) @@ -100,3 +114,5 @@ class LRUCacheTests: XCTestCase { } } + +#endif From 5482a84ca69f1e7f9d26188ab5a12eeca68e8d4d Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 20 Apr 2022 09:45:21 -0700 Subject: [PATCH 26/84] exclude performance tests from ci --- .../OptimizelyUserContextTests_Performance.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Performance.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Performance.swift index 10a655ac..3772b079 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Performance.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Performance.swift @@ -24,8 +24,13 @@ class OptimizelyUserContextTests_Performance: XCTestCase { optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, defaultLogLevel: .error) try! optimizely.start(datafile: datafile) } - - // MARK: - Performance +} + +// 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 { @@ -90,8 +95,11 @@ class OptimizelyUserContextTests_Performance: XCTestCase { XCTAssert(timeInMicrosecs < 300, "user context decide-with-valid takes too long (\(timeInMicrosecs) microsecs)") } + } +#endif + // MARK: - Utils extension OptimizelyUserContextTests_Performance { From 6a7019462bbb494316b53d3601be20f8980ee6af Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 22 Apr 2022 13:55:13 -0700 Subject: [PATCH 27/84] fix to support odp integration in datafile --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 40 ++++ .../AudienceSegmentsHandler.swift | 2 + .../AudienceSegments/ZaiusApiManager.swift | 6 +- Sources/Data Model/Integration.swift | 23 ++ Sources/Data Model/Project.swift | 6 +- Sources/Data Model/ProjectConfig.swift | 8 + .../OptimizelyUserContext.swift | 7 +- Sources/Optimizely/OptimizelyClient.swift | 16 +- .../Protocols/OPTAudienceSegmentHandler.swift | 1 + .../IntegrationTests.swift | 85 ++++++++ .../AudienceSegmentsHandlerTests.swift | 13 +- .../OptimizelyUserContextTests_Segments.swift | 200 ++++++++++++++++-- .../ZaiusApiManagerTests.swift | 72 ++----- .../ProjectTests.swift | 16 +- .../decide/decide_audience_segments.json | 7 + 15 files changed, 416 insertions(+), 86 deletions(-) create mode 100644 Sources/Data Model/Integration.swift create mode 100644 Tests/OptimizelyTests-APIs/IntegrationTests.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 74eb49d9..71a73a81 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1791,6 +1791,24 @@ 8428D3D7280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */; }; 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.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 */; }; 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 */; }; @@ -2250,6 +2268,8 @@ 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCacheTests.swift; sourceTree = ""; }; 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiusApiManagerTests.swift; sourceTree = ""; }; 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandlerTests.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 = ""; }; 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 = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; @@ -2623,6 +2643,7 @@ 6E75168D22C520D400B2B157 /* ProjectConfig.swift */, 6E75168E22C520D400B2B157 /* FeatureVariable.swift */, 6E75168F22C520D400B2B157 /* Rollout.swift */, + 8464086F28130D3200CCF97D /* Integration.swift */, 6E75169022C520D400B2B157 /* Variation.swift */, 6E75169122C520D400B2B157 /* TrafficAllocation.swift */, 6E75169222C520D400B2B157 /* Project.swift */, @@ -2845,6 +2866,7 @@ 6E7519B922C5211100B2B157 /* OptimizelyTests-APIs */ = { isa = PBXGroup; children = ( + 84640880281320F000CCF97D /* IntegrationTests.swift */, 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */, 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */, 6E7519BA22C5211100B2B157 /* OptimizelyClientTests_Evaluation.swift */, @@ -3971,6 +3993,7 @@ 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 */, @@ -4080,6 +4103,7 @@ 6E424CB926324B1D0081004A /* Constants.swift in Sources */, 6E424CBA26324B1D0081004A /* Notifications.swift in Sources */, 6E424CBB26324B1D0081004A /* MurmurHash3.swift in Sources */, + 8464087628130D3200CCF97D /* Integration.swift in Sources */, 6E424CBC26324B1D0081004A /* HandlerRegistryService.swift in Sources */, 6E424CBD26324B1D0081004A /* LogMessage.swift in Sources */, 6E424CBE26324B1D0081004A /* AtomicProperty.swift in Sources */, @@ -4110,6 +4134,7 @@ 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 */, 6E75171322C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75191922C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, @@ -4254,6 +4279,7 @@ 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 */, @@ -4333,6 +4359,7 @@ 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 */, @@ -4393,6 +4420,7 @@ files = ( 6EC6DD4A24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75170122C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, + 8464087B28130D3200CCF97D /* Integration.swift in Sources */, 6EF8DE3A24BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E7516B922C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E75175522C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4507,6 +4535,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 */, @@ -4657,6 +4686,7 @@ 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 */, @@ -4669,6 +4699,7 @@ 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 */, @@ -4755,6 +4786,7 @@ 6E75178122C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EC6DD4524ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E7517CB22C520D400B2B157 /* DefaultBucketer.swift in Sources */, + 8464087528130D3200CCF97D /* Integration.swift in Sources */, 6E5D12202638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E9B115022C5486E00C22D81 /* BucketTests_Base.swift in Sources */, 6E9B114522C5486E00C22D81 /* MurmurTests.swift in Sources */, @@ -4904,6 +4936,7 @@ 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 */, @@ -4916,6 +4949,7 @@ 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 */, @@ -5032,6 +5066,7 @@ 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 */, @@ -5126,6 +5161,7 @@ 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 */, @@ -5175,6 +5211,7 @@ 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 */, 6E75184022C520D400B2B157 /* Event.swift in Sources */, 6E7516E222C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, @@ -5319,6 +5356,7 @@ 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 */, @@ -5344,6 +5382,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 */, @@ -5443,6 +5482,7 @@ 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 */, BD6485462491474500F30986 /* Event.swift in Sources */, BD6485472491474500F30986 /* OPTEventDispatcher.swift in Sources */, diff --git a/Sources/AudienceSegments/AudienceSegmentsHandler.swift b/Sources/AudienceSegments/AudienceSegmentsHandler.swift index 062ebe96..da8823f5 100644 --- a/Sources/AudienceSegments/AudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/AudienceSegmentsHandler.swift @@ -29,6 +29,7 @@ class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { } func fetchQualifiedSegments(apiKey: String, + apiHost: String, userKey: String, userValue: String, segmentsToCheck: [String]? = nil, @@ -51,6 +52,7 @@ class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { } zaiusMgr.fetch(apiKey: apiKey, + apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: segmentsToCheck) { segments, err in diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 7daa0aec..e258aa2c 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -80,9 +80,8 @@ query MyQuery { class ZaiusApiManager { let logger = OPTLoggerFactory.getLogger() - let apiHost = "https://api.zaius.com/v3/graphql" - func fetch(apiKey: String, + apiHost: String, userKey: String, userValue: String, segmentsToCheck: [String]?, @@ -102,7 +101,8 @@ class ZaiusApiManager { return } - let url = URL(string: apiHost)! + let apiEndpoint = apiHost + "/v3/graphql" + let url = URL(string: apiEndpoint)! var urlRequest = URLRequest(url: url) urlRequest.httpMethod = "POST" urlRequest.httpBody = httpBody 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 2f9446da..c85ad6a2 100644 --- a/Sources/Data Model/Project.swift +++ b/Sources/Data Model/Project.swift @@ -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,7 +64,8 @@ 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 } } diff --git a/Sources/Data Model/ProjectConfig.swift b/Sources/Data Model/ProjectConfig.swift index 7fc988f1..0c0a1060 100644 --- a/Sources/Data Model/ProjectConfig.swift +++ b/Sources/Data Model/ProjectConfig.swift @@ -193,6 +193,14 @@ extension ProjectConfig { // old versions (< 4) of datafiles not supported return ["4"].contains(version) } + + var publicKeyForODP: String? { + return project.integrations?.filter { $0.key == "odp" }.first?.publicKey + } + + var hostForODP: String? { + return project.integrations?.filter { $0.key == "odp" }.first?.host + } } // MARK: - Project Access diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index f6f9273e..037fe3e6 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -172,12 +172,14 @@ extension OptimizelyUserContext { /// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time. /// /// - Parameters: - /// - apiKey: the public API key for the ODP account from which the audience segments will be fetched. + /// - apiKey: The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile. + /// - apiHost: The host URL for the ODP audience segments API (optional). If not provided, SDK will use the default host in datafile. /// - userKey: The name of the user identifier (optional). /// - userValue: The value of the user identifier (optional). /// - 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(apiKey: String, + public func fetchQualifiedSegments(apiKey: String? = nil, + apiHost: String? = nil, userKey: String? = nil, userValue: String? = nil, options: [OptimizelySegmentOption] = [], @@ -191,6 +193,7 @@ extension OptimizelyUserContext { let userValue = userValue ?? userId optimizely.fetchQualifiedSegments(apiKey: apiKey, + apiHost: apiHost, userKey: userKey, userValue: userValue, options: options) { segments, err in diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 77ce9b16..ebef79d2 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -778,15 +778,27 @@ open class OptimizelyClient: NSObject { // MARK: - AudienceSegmentsHandler - func fetchQualifiedSegments(apiKey: String, + func fetchQualifiedSegments(apiKey: String?, + apiHost: String?, userKey: String, userValue: String, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + guard let odpApiKey = apiKey ?? config?.publicKeyForODP else { + completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) + return + } + + guard let odpApiHost = apiHost ?? config?.hostForODP else { + completionHandler(nil, .fetchSegmentsFailed("apiHost not defined")) + return + } + let segmentsToCheck = options.contains(.useSubset) ? config?.allSegments : nil - audienceSegmentsHandler.fetchQualifiedSegments(apiKey: apiKey, + audienceSegmentsHandler.fetchQualifiedSegments(apiKey: odpApiKey, + apiHost: odpApiHost, userKey: userKey, userValue: userValue, segmentsToCheck: segmentsToCheck, diff --git a/Sources/Protocols/OPTAudienceSegmentHandler.swift b/Sources/Protocols/OPTAudienceSegmentHandler.swift index c3135faa..609c2e1f 100644 --- a/Sources/Protocols/OPTAudienceSegmentHandler.swift +++ b/Sources/Protocols/OPTAudienceSegmentHandler.swift @@ -19,6 +19,7 @@ import Foundation protocol OPTAudienceSegmentsHandler { func fetchQualifiedSegments(apiKey: String, + apiHost: String, userKey: String, userValue: String, segmentsToCheck: [String]?, diff --git a/Tests/OptimizelyTests-APIs/IntegrationTests.swift b/Tests/OptimizelyTests-APIs/IntegrationTests.swift new file mode 100644 index 00000000..5ceacdd0 --- /dev/null +++ b/Tests/OptimizelyTests-APIs/IntegrationTests.swift @@ -0,0 +1,85 @@ +// +// 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 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-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift index 04fe03af..154b4a04 100644 --- a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift @@ -21,6 +21,8 @@ class AudienceSegmentsHandlerTests: XCTestCase { var options = [OptimizelySegmentOption]() var apiKey = "valid" + var apiHost = "host" + var userKey = "vuid" var userValue = "test-user" @@ -33,7 +35,7 @@ class AudienceSegmentsHandlerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + handler.fetchQualifiedSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil, options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["new-customer"], segments) @@ -48,7 +50,7 @@ class AudienceSegmentsHandlerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + handler.fetchQualifiedSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil, options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["a"], segments) @@ -61,7 +63,7 @@ class AudienceSegmentsHandlerTests: XCTestCase { func testError() { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: "invalid-key", userKey: userKey, userValue: userValue, + handler.fetchQualifiedSegments(apiKey: "invalid-key", apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil, options: []) { segments, error in XCTAssertNotNil(error) XCTAssert(segments!.isEmpty ) @@ -80,7 +82,7 @@ class AudienceSegmentsHandlerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + handler.fetchQualifiedSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil, options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["new-customer"], segments, "cache lookup should be skipped") @@ -99,7 +101,7 @@ class AudienceSegmentsHandlerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, userKey: userKey, userValue: userValue, + handler.fetchQualifiedSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil, options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["new-customer"], segments, "cache lookup should be skipped") @@ -131,6 +133,7 @@ class AudienceSegmentsHandlerTests: XCTestCase { class MockZaiusApiManager: ZaiusApiManager { override func fetch(apiKey: String, + apiHost: String, userKey: String, userValue: String, segmentsToCheck: [String]?, diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index ff1e4d79..a40ec8f8 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -21,15 +21,21 @@ class OptimizelyUserContextTests_Segments: XCTestCase { var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) var segmentHandler = MockAudienceSegmentsHandler(cacheSize: 100, cacheTimeoutInSecs: 100) var user: OptimizelyUserContext! - let kApiKey = "any-key" + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + let kUserId = "tester" - let kUserIdKey = "$opt_user_id" + let kApiKey = "any-key" + let kApiHost = "any-host" + let kUserKey = "custom_id" + let kUserValue = "custom_id_value" override func setUp() { optimizely.audienceSegmentsHandler = segmentHandler user = optimizely.createUserContext(userId: kUserId) } + // MARK: - isQualifiedFor + func testIsQualifiedFor() { XCTAssertFalse(user.isQualifiedFor(segment: "a")) @@ -40,23 +46,27 @@ class OptimizelyUserContextTests_Segments: XCTestCase { user.qualifiedSegments = [] XCTAssertFalse(user.isQualifiedFor(segment: "a")) } + + // MARK: - Success func testFetchQualifiedSegments_successDefaultUser() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { segments, error in + user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { segments, error in XCTAssertNil(error) - XCTAssert(segments == [self.kApiKey, self.kUserIdKey, self.kUserId]) + XCTAssert(segments == ["segment-1"]) XCTAssert(self.user.qualifiedSegments == segments) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) } + // MARK: - Failure + func testFetchQualifiedSegments_sdkNotReady() { user.optimizely = nil let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { segments, error in + user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { segments, error in XCTAssertEqual(OptimizelyError.sdkNotReady.reason, error?.reason) XCTAssertNil(segments) XCTAssertNil(self.user.qualifiedSegments) @@ -67,7 +77,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_fetchFailed() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: "invalid-key") { segments, error in + user.fetchQualifiedSegments(apiKey: "invalid-key", apiHost: kApiHost) { segments, error in XCTAssertNotNil(error) XCTAssertNil(segments) XCTAssertNil(self.user.qualifiedSegments) @@ -76,9 +86,11 @@ class OptimizelyUserContextTests_Segments: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) } + // MARK: - SegmentsToCheck + func testFetchQualifiedSegments_segmentsToCheck_emptyBeforeStart() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { _, _ in + user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -91,7 +103,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { try? optimizely.start(datafile: datafile) let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { _, _ in + user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -101,7 +113,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_segmentsToCheck_emptyBeforeStart_withUseSubsetOption() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, options: [.useSubset]) { _, _ in + user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost, options: [.useSubset]) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -116,7 +128,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { // fetch segments after SDK initialized, so segmentsToCheck will be used. let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, options: [.useSubset]) { _, _ in + user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost, options: [.useSubset]) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -124,6 +136,8 @@ class OptimizelyUserContextTests_Segments: XCTestCase { XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!)) } + // MARK: - Customisze AudienceSegmentHandler + func testCustomizeAudienceSegmentsHandler() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, periodicDownloadInterval: 60, @@ -136,28 +150,174 @@ class OptimizelyUserContextTests_Segments: XCTestCase { } +// MARK: - Optional parameters + +extension OptimizelyUserContextTests_Segments { + + func testFetchQualifiedSegments_parameters() { + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: kApiKey, + apiHost: kApiHost, + userKey: kUserKey, + userValue: kUserValue, + options: [.ignoreCache]) { _, _ in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + + XCTAssertEqual(kApiKey, segmentHandler.apiKey) + XCTAssertEqual(kApiHost, segmentHandler.apiHost) + XCTAssertEqual(kUserKey, segmentHandler.userKey) + XCTAssertEqual(kUserValue, segmentHandler.userValue) + XCTAssertNil(segmentHandler.segmentsToCheck) + XCTAssertEqual([.ignoreCache], segmentHandler.options) + } + + func testFetchQualifiedSegments_defaults_configReady() { + try! optimizely.start(datafile: datafile) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments() { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, ["segment-1"]) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + + XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", optimizely.config?.publicKeyForODP, "apiKey from datafile should be used as a default") + XCTAssertEqual("https://api.zaius.com", optimizely.config?.hostForODP, "apiHost from datafile should be used as a default") + XCTAssertEqual("$opt_user_id", segmentHandler.userKey, "the reserved user-key should be used as a default") + XCTAssertEqual(kUserId, segmentHandler.userValue, "userId should be used as a default") + XCTAssertEqual(nil, segmentHandler.segmentsToCheck, "segmentsToCheck should be nil as a default") + XCTAssertEqual([], segmentHandler.options) + } + + func testFetchQualifiedSegments_missingApiKey_configNotReady() { + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments() { segments, error in + if let error = error, case OptimizelyError.fetchSegmentsFailed(let hint) = error { + XCTAssertEqual("apiKey not defined", hint) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + + func testFetchQualifiedSegments_missingApiHost_configNotReady() { + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: kApiKey) { segments, error in + if let error = error, case OptimizelyError.fetchSegmentsFailed(let hint) = error { + XCTAssertEqual("apiHost not defined", hint) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + + func testFetchQualifiedSegments_defaults_configReady_missingIntegration() { + let datafile = OTUtils.loadJSONDatafile("decide_datafile")! // no integration in this datafile + try! optimizely.start(datafile: datafile) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments() { segments, error in + if let error = error, case OptimizelyError.fetchSegmentsFailed(let hint) = error { + XCTAssertEqual("apiKey not defined", hint) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) + } + +} + // MARK: - MockAudienceSegmentsHandler class MockAudienceSegmentsHandler: AudienceSegmentsHandler { + var apiKey: String? + var apiHost: String? + var userKey: String? + var userValue: String? var segmentsToCheck: [String]? - + var options: [OptimizelySegmentOption]? + override func fetchQualifiedSegments(apiKey: String, - userKey: String, - userValue: String, - segmentsToCheck: [String]?, - options: [OptimizelySegmentOption], - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + apiHost: String, + userKey: String, + userValue: String, + segmentsToCheck: [String]?, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + + self.apiKey = apiKey + self.apiHost = apiHost + self.userKey = userKey + self.userValue = userValue + self.segmentsToCheck = segmentsToCheck + self.options = options + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - self.segmentsToCheck = segmentsToCheck - if apiKey == "invalid-key" { completionHandler(nil, OptimizelyError.generic) } else { - // pass back [key, userKey, userValue] in segments for validation - let sampleSegments = [apiKey, userKey, userValue] + let sampleSegments = ["segment-1"] completionHandler(sampleSegments, nil) } } } } +// MARK: - Tests with real ODP server +// TODO: this test can be flaky. replace it with a good test account or remove it later. + +extension OptimizelyUserContextTests_Segments { + + func testLiveODPGraphQL() { + let testODPApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" + let testODPUserValue = "d66a9d81923d4d2f99d8f64338976322" + let testODPUserKey = "vuid" + let testODPApiHost = "https://api.zaius.com" + + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + try! optimizely.start(datafile: datafile) + let user = optimizely.createUserContext(userId: kUserId) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: testODPApiKeyForAudienceSegments, + apiHost: testODPApiHost, + userKey: testODPUserKey, + userValue: testODPUserValue) { segments, error in + XCTAssertNil(error) + XCTAssert(segments!.contains("has_email")) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + func testLiveODPGraphQL_defaultParameters() { + let testODPUserKey = "vuid" + let testODPUserValue = "d66a9d81923d4d2f99d8f64338976322" + + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + try! optimizely.start(datafile: datafile) + let user = optimizely.createUserContext(userId: kUserId) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(userKey: testODPUserKey, + userValue: testODPUserValue) { segments, error in + XCTAssertNil(error) + XCTAssert(segments!.contains("has_email")) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + +} diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift index 23efd20e..69ae9cba 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -24,6 +24,7 @@ class ZaiusApiManagerTests: XCTestCase { let userValue = "test-user-value" let apiKey = "test-api-key" + let apiHost = "https://test-host" static var createdApiRequest: URLRequest? @@ -31,19 +32,34 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodResponseData)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in XCTAssertNil(error) XCTAssertEqual(segments, ["qualified-and-ready"]) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + guard let request = ZaiusApiManagerTests.createdApiRequest else { + XCTFail() + return + } + + let expectedBody = [ + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state}}}}}" + ] + + XCTAssertEqual(apiHost + "/v3/graphql", request.url?.absoluteString) + XCTAssertEqual("POST", request.httpMethod) + XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) + XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) + XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) } func testFetchQualifiedSegments_successWithEmptySegments() { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodEmptyResponseData)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in XCTAssertNil(error) XCTAssertEqual(segments, []) sem.signal() @@ -55,7 +71,7 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in if case .fetchSegmentsFailed("decode error") = error { XCTAssert(true) } else { @@ -71,7 +87,7 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in if case .fetchSegmentsFailed("download failed") = error { XCTAssert(true) } else { @@ -83,35 +99,11 @@ class ZaiusApiManagerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - func testGraphQLRequest_allSegments() { - let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) - - let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { _, _ in - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - - guard let request = ZaiusApiManagerTests.createdApiRequest else { - XCTFail() - return - } - - let expectedBody = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state}}}}}" - ] - - XCTAssertEqual("POST", request.httpMethod) - XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) - XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) - XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) - } - func testGraphQLRequest_subsetSegments() { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, userKey: userKey, userValue: userValue, segmentsToCheck: ["a", "b"]) { _, _ in + manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: ["a", "b"]) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) @@ -145,29 +137,7 @@ class ZaiusApiManagerTests: XCTestCase { XCTAssertNil(dict.extractComponent(keyPath: "a.b.c.d")) XCTAssertNil(dict.extractComponent(keyPath: "d")) } - - // MARK: - Tests with real ODP server - - // TODO: this test can be flaky. replace it with a good test account or remove it later. - - func testLiveODPGraphQL() { - let testODPApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" - let testODPUserIdForAudienceSegments = "d66a9d81923d4d2f99d8f64338976322" - - let manager = ZaiusApiManager() - let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: testODPApiKeyForAudienceSegments, - userKey: "vuid", - userValue: testODPUserIdForAudienceSegments, - segmentsToCheck: ["has_email", "has_email_opted_in", "invalid-segment"]) { segments, error in - XCTAssertNil(error) - XCTAssertEqual(Set(["has_email", "has_email_opted_in"]), Set(segments!)) - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) - } - // MARK: - MockZaiusApiManager class MockZaiusApiManager: ZaiusApiManager { 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/TestData/decide/decide_audience_segments.json b/Tests/TestData/decide/decide_audience_segments.json index 0b9d4f24..826ae361 100644 --- a/Tests/TestData/decide/decide_audience_segments.json +++ b/Tests/TestData/decide/decide_audience_segments.json @@ -103,6 +103,13 @@ } ], "groups": [], + "integrations": [ + { + "key": "odp", + "host": "https://api.zaius.com", + "publicKey": "W4WzcEs-ABgXorzY7h1LCQ" + } + ], "typedAudiences": [ { "id": "13389142234", From 93ca111893396969aab7c7d4b9c675d423a054d2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 2 May 2022 09:00:34 -0700 Subject: [PATCH 28/84] fix datafile error --- Tests/TestData/decide/decide_audience_segments.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/TestData/decide/decide_audience_segments.json b/Tests/TestData/decide/decide_audience_segments.json index 826ae361..4427915c 100644 --- a/Tests/TestData/decide/decide_audience_segments.json +++ b/Tests/TestData/decide/decide_audience_segments.json @@ -84,7 +84,7 @@ } ], "audienceIds": ["$opt_dummy_audience"], - "audienceConditions": ["or", "13389141123", "13389142234"], + "audienceConditions": ["or", "13389142234", "13389141123"], "variations": [ { "variables": [], @@ -148,7 +148,7 @@ "value": "us", "type": "custom_attribute", "name": "country", - "match": "equal" + "match": "exact" } ], [ From 2333bd0417588631c5bc2389487f28cc327d2002 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 3 May 2022 14:04:10 -0700 Subject: [PATCH 29/84] clean up per zaius api changes --- .../AudienceSegments/ZaiusApiManager.swift | 14 +++----- .../ZaiusApiManagerTests.swift | 34 +++++-------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index e258aa2c..01143696 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -25,10 +25,10 @@ import Foundation /* GraphQL Request // fetch all segments -curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name is_ready state}}}}}"}' https://api.zaius.com/v3/graphql +curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql // fetch info for "has_email" segment only -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"]) {edges {node {name is_ready state}}}}}"}' https://api.zaius.com/v3/graphql +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"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql query MyQuery { customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { @@ -36,7 +36,6 @@ query MyQuery { edges { node { name - is_ready state description } @@ -56,7 +55,6 @@ query MyQuery { { "node": { "name": "has_email", - "is_ready": true, "state": "qualified", "description": "Customers who have an email address (regardless of consent/reachability status)" } @@ -64,7 +62,6 @@ query MyQuery { { "node": { "name": "has_email_opted_in", - "is_ready": true, "state": "qualified", "description": "Customers who have an email address, and it is opted-in" } @@ -94,7 +91,7 @@ class ZaiusApiManager { let subsetFilter = makeSubsetFilter(segments: segmentsToCheck) let body = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences\(subsetFilter) {edges {node {name is_ready state}}}}}" + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences\(subsetFilter) {edges {node {name state}}}}}" ] guard let httpBody = try? JSONEncoder().encode(body) else { completionHandler(nil, .fetchSegmentsFailed("invalid query.")) @@ -171,22 +168,19 @@ class ZaiusApiManager { struct ODPAudience: Decodable { let name: String - let isReady: Bool let state: String let description: String? // optional so we can add for debugging var isQualified: Bool { - return isReady && (state == "qualified") + return (state == "qualified") } init?(_ dict: [String: Any]?) { guard let dict = dict, let name = dict["name"] as? String, - let isReady = dict["is_ready"] as? Bool, let state = dict["state"] as? String else { return nil } self.name = name - self.isReady = isReady self.state = state self.description = dict["description"] as? String } diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift index 69ae9cba..e957cfd6 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -34,7 +34,7 @@ class ZaiusApiManagerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in XCTAssertNil(error) - XCTAssertEqual(segments, ["qualified-and-ready"]) + XCTAssertEqual(segments, ["qualified"]) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) @@ -45,7 +45,7 @@ class ZaiusApiManagerTests: XCTestCase { } let expectedBody = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name is_ready state}}}}}" + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name state}}}}}" ] XCTAssertEqual(apiHost + "/v3/graphql", request.url?.absoluteString) @@ -114,7 +114,7 @@ class ZaiusApiManagerTests: XCTestCase { } let expectedBody = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences(subset:[\"a\",\"b\"]) {edges {node {name is_ready state}}}}}" + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences(subset:[\"a\",\"b\"]) {edges {node {name state}}}}}" ] XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) @@ -210,34 +210,16 @@ class ZaiusApiManagerTests: XCTestCase { "edges": [ { "node": { - "name": "qualified-and-ready", - "is_ready": true, + "name": "qualified", "state": "qualified", - "description": "qualifed and ready" + "description": "qualifed sample" } }, { "node": { - "name": "qualified-and-not-ready", - "is_ready": false, - "state": "qualified", - "description": "qualified and not-ready" - } - }, - { - "node": { - "name": "not-qualified-and-ready", - "is_ready": false, - "state": "qualified", - "description": "not-qualified and ready" - } - }, - { - "node": { - "name": "not-qualified-and-not-ready", - "is_ready": false, - "state": "qualified", - "description": "not-qualified and not-ready" + "name": "not-qualified", + "state": "not_qualified", + "description": "not-qualified sample" } } ] From 7a964bd6faba5d786841695ca5df531907a5f4e2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 6 May 2022 15:52:30 -0700 Subject: [PATCH 30/84] samples for odp register --- DemoSwiftApp/AppDelegate.swift | 30 +++- DemoSwiftApp/Samples/SamplesForAPI.swift | 2 +- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 42 ++++++ Sources/ODP/ODPManager.swift | 141 +++++++++++++++++++ Sources/Optimizely/OptimizelyClient.swift | 2 + 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 Sources/ODP/ODPManager.swift diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index b7bd5436..4bc45ca0 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -130,7 +130,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @unknown default: print("Optimizely SDK initiliazation failed with unknown result") } - self.startWithRootViewController() + + let odp = self.optimizely.odpManager + print("VUID: \(odp.vuid)") + + odp.register { success in + guard success else { + print("ODP register failed") + return + } + + print("ODP register done") + + if odp.isUserRegistered(userId: self.userId) { + print("ODP identify already done - skip identify sync step") + } else { + odp.identify(userId: self.userId) { success in + guard success else { + print("ODP identify failed") + return + } + + print("ODP identify done") + } + } + + } + + + //self.startWithRootViewController() // For sample codes for APIs, see "Samples/SamplesForAPI.swift" //SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely) diff --git a/DemoSwiftApp/Samples/SamplesForAPI.swift b/DemoSwiftApp/Samples/SamplesForAPI.swift index fcb3a13c..b04bbffb 100644 --- a/DemoSwiftApp/Samples/SamplesForAPI.swift +++ b/DemoSwiftApp/Samples/SamplesForAPI.swift @@ -276,7 +276,7 @@ class SamplesForAPI { } let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"]) - user.fetchQualifiedSegments(apiKey: "sample-api-key", options: [.ignoreCache]) { error in + user.fetchQualifiedSegments(apiKey: "sample-api-key", options: [.ignoreCache]) { _, error in guard error == nil else { print("[AudienceSegments] \(error!.errorDescription!)") return diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 71a73a81..3cf86a1c 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1769,6 +1769,22 @@ 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 */; }; + 84037D4228257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4328257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4428257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4528257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4628257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4728257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4828257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4928257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4A28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4B28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4C28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4D28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4E28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D4F28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D5028257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; + 84037D5128257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; 8405834E2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; 8405834F2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; 840583502808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; @@ -2264,6 +2280,7 @@ 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 = ""; }; + 84037D4128257F6C00B33062 /* ODPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ODPManager.swift; sourceTree = ""; }; 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPTAudienceSegmentHandler.swift; sourceTree = ""; }; 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCacheTests.swift; sourceTree = ""; }; 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiusApiManagerTests.swift; sourceTree = ""; }; @@ -2507,6 +2524,7 @@ 6E75165D22C520D400B2B157 /* Sources */ = { isa = PBXGroup; children = ( + 84037D5228257F7500B33062 /* ODP */, 6E75166622C520D400B2B157 /* Optimizely */, 6EC6DD3F24ABF8180017D296 /* Optimizely+Decide */, 6E75165E22C520D400B2B157 /* Customization */, @@ -2928,6 +2946,14 @@ path = watchOS; sourceTree = ""; }; + 84037D5228257F7500B33062 /* ODP */ = { + isa = PBXGroup; + children = ( + 84037D4128257F6C00B33062 /* ODPManager.swift */, + ); + path = ODP; + sourceTree = ""; + }; 87DE4DE091B80D1F13BBD781 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -3982,6 +4008,7 @@ 6EF8DE1F24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E14CD882423F9A100010234 /* AttributeValue.swift in Sources */, 6E14CD822423F9A100010234 /* DataStoreFile.swift in Sources */, + 84037D4928257F6C00B33062 /* ODPManager.swift in Sources */, 6E14CDA42423F9C300010234 /* Notifications.swift in Sources */, 6E20050B26B4D28500278087 /* MockLogger.swift in Sources */, 6E14CD872423F9A100010234 /* Audience.swift in Sources */, @@ -4076,6 +4103,7 @@ 6E424D5026324C4D0081004A /* OptimizelyDecideOption.swift in Sources */, 6E424D5126324C4D0081004A /* OptimizelyDecision.swift in Sources */, 6E424D1526324B620081004A /* DataStoreQueueStack.swift in Sources */, + 84037D4828257F6C00B33062 /* ODPManager.swift in Sources */, 6E424D1626324B620081004A /* OPTDataStore.swift in Sources */, 6E424D2F26324BBA0081004A /* OTUtils.swift in Sources */, 6E424D1726324B620081004A /* OPTDecisionService.swift in Sources */, @@ -4174,6 +4202,7 @@ 6E75177F22C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E75181122C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE1B24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 84037D4328257F6C00B33062 /* ODPManager.swift in Sources */, 6E75173722C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E7517F922C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, C78CAF592445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4268,6 +4297,7 @@ 6EF8DE2424BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75183022C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75192022C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, + 84037D4E28257F6C00B33062 /* ODPManager.swift in Sources */, 6E9B117922C5487A00C22D81 /* tvOSOnlyTests.swift in Sources */, 6E20051026B4D28500278087 /* MockLogger.swift in Sources */, 6E7518B422C520D400B2B157 /* Group.swift in Sources */, @@ -4363,6 +4393,7 @@ 6E5D12232638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6ECB60C6234D329500016D41 /* OptimizelyClientTests_OptimizelyConfig.swift in Sources */, 6EA2CC282345618E001E7531 /* OptimizelyConfig.swift in Sources */, + 84037D4A28257F6C00B33062 /* ODPManager.swift in Sources */, 6E75189822C520D400B2B157 /* Experiment.swift in Sources */, 6E8A3D4C2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 84B4D75827E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, @@ -4483,6 +4514,7 @@ 6E75176D22C520D400B2B157 /* Utils.swift in Sources */, 6EA2CC2B2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E75182322C520D400B2B157 /* BatchEventBuilder.swift in Sources */, + 84037D4D28257F6C00B33062 /* ODPManager.swift in Sources */, 6EF8DE1524BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E424C06263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75174922C520D400B2B157 /* HandlerRegistryService.swift in Sources */, @@ -4530,6 +4562,7 @@ 0B97DDA4249D4A27003DE606 /* SemanticVersion.swift in Sources */, 6ECB60D3234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B116C22C5487100C22D81 /* BucketTests_ExpToVariation.swift in Sources */, + 84037D4F28257F6C00B33062 /* ODPManager.swift in Sources */, 6E7E9C4C25240D31009E4426 /* OptimizelyUserContextTests_Objc.m in Sources */, 6E75193922C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7518C122C520D400B2B157 /* Variable.swift in Sources */, @@ -4699,6 +4732,7 @@ 6E7516EC22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75181A22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE2624BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 84037D5028257F6C00B33062 /* ODPManager.swift in Sources */, 8464087E28130D3200CCF97D /* Integration.swift in Sources */, 6E9B119722C5488300C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184A22C520D400B2B157 /* Event.swift in Sources */, @@ -4873,6 +4907,7 @@ 6E9B114D22C5486E00C22D81 /* BatchEventBuilderTests_Events.swift in Sources */, 6E75188B22C520D400B2B157 /* Project.swift in Sources */, 6E75187F22C520D400B2B157 /* TrafficAllocation.swift in Sources */, + 84037D4728257F6C00B33062 /* ODPManager.swift in Sources */, 6E0207A8272A11CF008C3711 /* NetworkReachabilityTests.swift in Sources */, 6E7518BB22C520D400B2B157 /* Variable.swift in Sources */, 6E7518AF22C520D400B2B157 /* Group.swift in Sources */, @@ -4949,6 +4984,7 @@ 6E7516E722C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75181522C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE2124BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 84037D4B28257F6C00B33062 /* ODPManager.swift in Sources */, 8464087928130D3200CCF97D /* Integration.swift in Sources */, 6E9B118122C5488100C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184522C520D400B2B157 /* Event.swift in Sources */, @@ -5076,6 +5112,7 @@ 6E9B11AF22C5489400C22D81 /* MockUrlSession.swift in Sources */, 6E7516F422C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75195A22C520D500B2B157 /* OPTBucketer.swift in Sources */, + 84037D4C28257F6C00B33062 /* ODPManager.swift in Sources */, 6E75177822C520D400B2B157 /* SDKVersion.swift in Sources */, 6E75194E22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75173C22C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5171,6 +5208,7 @@ 6E9B11B922C5489700C22D81 /* MockUrlSession.swift in Sources */, 6E7516F922C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75195F22C520D500B2B157 /* OPTBucketer.swift in Sources */, + 84037D5128257F6C00B33062 /* ODPManager.swift in Sources */, 6E75177D22C520D400B2B157 /* SDKVersion.swift in Sources */, 6E75195322C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75174122C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5251,6 +5289,7 @@ 6E75173622C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E75185822C520D400B2B157 /* FeatureVariable.swift in Sources */, 6EF8DE1A24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 84037D4228257F6C00B33062 /* ODPManager.swift in Sources */, 6E75186422C520D400B2B157 /* Rollout.swift in Sources */, 6E75179622C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, C78CAF582445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -5345,6 +5384,7 @@ 6EF8DE1D24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7518EA22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E75182A22C520D400B2B157 /* BatchEvent.swift in Sources */, + 84037D4628257F6C00B33062 /* ODPManager.swift in Sources */, 6E75191A22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E20050826B4D28500278087 /* MockLogger.swift in Sources */, 6E7518AE22C520D400B2B157 /* Group.swift in Sources */, @@ -5453,6 +5493,7 @@ 75C71A3A25E454460084187E /* OPTBucketer.swift in Sources */, 75C71A3B25E454460084187E /* ArrayEventForDispatch+Extension.swift in Sources */, 75C71A3C25E454460084187E /* OptimizelyClient+Extension.swift in Sources */, + 84037D4528257F6C00B33062 /* ODPManager.swift in Sources */, 75C71A3D25E454460084187E /* DataStoreQueueStackImpl+Extension.swift in Sources */, 75C71A3E25E454460084187E /* Array+Extension.swift in Sources */, 75C71A3F25E454460084187E /* Constants.swift in Sources */, @@ -5522,6 +5563,7 @@ BD6485622491474500F30986 /* MurmurHash3.swift in Sources */, BD6485632491474500F30986 /* FeatureVariable.swift in Sources */, 6EF8DE1C24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 84037D4428257F6C00B33062 /* ODPManager.swift in Sources */, BD6485642491474500F30986 /* Rollout.swift in Sources */, BD6485652491474500F30986 /* DataStoreQueueStackImpl+Extension.swift in Sources */, BD6485662491474500F30986 /* OptimizelyJSON.swift in Sources */, diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift new file mode 100644 index 00000000..148aa3e8 --- /dev/null +++ b/Sources/ODP/ODPManager.swift @@ -0,0 +1,141 @@ +// +// 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 class ODPManager { + let keyForVuid = "vuid" + let keyForVuidUsers = "vuid-users" + + var odpPublicKey: String { + return "W4WzcEs-ABgXorzY7h1LCQ" + } + var odpHost: String { + return "https://api.zaius.com" + } + + public let vuid: String + var usersRegistered: Set + + init() { + if let vuid = UserDefaults.standard.string(forKey: keyForVuid) { + self.vuid = vuid + } else { + let vuid = UUID().uuidString.replacingOccurrences(of: "-", with: "") + print("vuid generated: \(vuid)") + UserDefaults.standard.set(vuid, forKey: keyForVuid) + self.vuid = vuid + } + + if let users = UserDefaults.standard.object(forKey: keyForVuidUsers) as? [String] { + self.usersRegistered = Set(users) + } else { + self.usersRegistered = [] + } + } + + public func isUserRegistered(userId: String) -> Bool { + return usersRegistered.contains(userId) + } + + private func updateRegisteredUsers(userId: String) { + if usersRegistered.count > 10 { + usersRegistered = usersRegistered.filter { _ in Bool.random() } + } + + usersRegistered.insert(userId) + UserDefaults.standard.set(Array(usersRegistered), forKey: keyForVuidUsers) + UserDefaults.standard.synchronize() + print("saved users: \(usersRegistered)") + } + + public func register(completion: ((Bool) -> Void)? = nil) { + odpEvent(userId: nil, kind: "experimentation:client_initialized") { success in + if !success { + print("[ODP] register failed") + } + completion?(success) + } + } + + public func identify(userId: String, completion: ((Bool) -> Void)? = nil) { + odpEvent(userId: userId, kind: "experimentation:identified") { success in + if success { + print("[ODP] add idenfier (\(userId)) successfully") + + self.updateRegisteredUsers(userId: userId) + } + completion?(success) + } + } + + public func odpEvent(userId: String?, kind: String, data: [String: Any] = [:], completion: @escaping (Bool) -> Void) { + var identifiers = [ + "vuid": vuid + ] + + if let userId = userId { + identifiers["fs_user_id"] = userId + } + + let kinds = kind.split(separator: ":") + guard kinds.count == 2 else { + print("[ODP Event] invalid format for kind") + completion(false) + return + } + + guard let url = URL(string: "https://api.zaius.com/v3/events") else { + print("[ODP Event] invalid url") + completion(false) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + + let combinedData: [String: Any] = [ + "type": kinds[0], + "action": kinds[1], + "data_source": "fullstack:swift-sdk", + "identifiers": identifiers, + "data": data + ] + + guard let body = try? JSONSerialization.data(withJSONObject: combinedData) else { + print("[ODP Event] invalid JSON") + completion(false) + return + } + request.httpBody = body + request.addValue(odpPublicKey, forHTTPHeaderField: "x-api-key") + request.addValue("application/json", forHTTPHeaderField: "content-type") + +// let task = URLSession.shared.dataTask(with: url) { data, response, error in +// if let error = error { +// print("[ODP Event] API error: \(error)") +// completion(false) +// return +// } +// +// completion(true) +// } +// task.resume() + + completion(true) + } + +} diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index ebef79d2..8bd0e56e 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -71,6 +71,8 @@ open class OptimizelyClient: NSObject { atomicAudienceSegmentsHandler.property = newValue } } + + public var odpManager = ODPManager() // MARK: - Public interfaces From 92cd072a9b2dd44bd04b4ba3c19bfa03b05547a2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 10 May 2022 11:15:05 -0700 Subject: [PATCH 31/84] remove useSubset option --- .../OptimizelySegmentOption.swift | 2 -- Sources/Optimizely/OptimizelyClient.swift | 10 ++++++- .../AudienceSegmentsHandlerTests.swift | 1 - .../OptimizelyUserContextTests_Segments.swift | 27 ++++++------------- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Sources/AudienceSegments/OptimizelySegmentOption.swift b/Sources/AudienceSegments/OptimizelySegmentOption.swift index 12e12712..dd186ca1 100644 --- a/Sources/AudienceSegments/OptimizelySegmentOption.swift +++ b/Sources/AudienceSegments/OptimizelySegmentOption.swift @@ -18,8 +18,6 @@ import Foundation /// Options controlling audience segments. @objc public enum OptimizelySegmentOption: Int { - // fetch subset segments only (used in the project). default is fetch-all-segments - case useSubset // ignore cache (save/lookup) case ignoreCache // reset cache diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index ebef79d2..43131855 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -795,7 +795,15 @@ open class OptimizelyClient: NSObject { return } - let segmentsToCheck = options.contains(.useSubset) ? config?.allSegments : nil + let segmentsToCheck = config?.allSegments + + // filter out empty segmentsToCheck (segments not used in the project). + // pass to zaius for non-empty segments or nil (to access all segments) only. + + if let subset = segmentsToCheck, subset.isEmpty { + completionHandler([], nil) + return + } audienceSegmentsHandler.fetchQualifiedSegments(apiKey: odpApiKey, apiHost: odpApiHost, diff --git a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift index 154b4a04..eac52770 100644 --- a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift @@ -74,7 +74,6 @@ class AudienceSegmentsHandlerTests: XCTestCase { } // MARK: - OptimizelySegmentOption - // NOTE: The "useSubset" option is fully tested in "OptimizelyUserContextTests_Segments", so skip here. func testOptions_ignoreCache() { setCache(userKey, userValue, ["a"]) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index a40ec8f8..dfecc89f 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -98,7 +98,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { XCTAssertNil(segmentHandler.segmentsToCheck) } - func testFetchQualifiedSegments_segmentsToCheck_emptyAfterStart() { + func testFetchQualifiedSegments_segmentsToCheck_validAfterStart() { let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! try? optimizely.start(datafile: datafile) @@ -108,34 +108,23 @@ class OptimizelyUserContextTests_Segments: XCTestCase { } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - XCTAssertNil(segmentHandler.segmentsToCheck) - } - - func testFetchQualifiedSegments_segmentsToCheck_emptyBeforeStart_withUseSubsetOption() { - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost, options: [.useSubset]) { _, _ in - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - - XCTAssertNil(segmentHandler.segmentsToCheck) + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!)) } - func testFetchQualifiedSegments_segmentsToCheck_validAfterStart_withUseSubsetOption() { - let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + func testFetchQualifiedSegments_segmentsToCheck_segmentsNotUsed() { + let datafile = OTUtils.loadJSONDatafile("decide_datafile")! try? optimizely.start(datafile: datafile) - - // fetch segments after SDK initialized, so segmentsToCheck will be used. let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost, options: [.useSubset]) { _, _ in + user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!)) + XCTAssertNil(segmentHandler.segmentsToCheck, "empty segmentsToCheck case should be filtered out before calling segmentHandler") + XCTAssertEqual([], user.qualifiedSegments) } - + // MARK: - Customisze AudienceSegmentHandler func testCustomizeAudienceSegmentsHandler() { From b4e2862be6fa91d517f44b2b753fae7b7cfd9c1b Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 10 May 2022 14:21:46 -0700 Subject: [PATCH 32/84] fix branch ref in github action workflow --- .github/workflows/swift.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1ac7229d..05c74eaa 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,11 +22,11 @@ env: jobs: lint_markdown_files: - uses: optimizely/swift-sdk/.github/workflows/lint_markdown.yml@yasir/gitAction + uses: optimizely/swift-sdk/.github/workflows/lint_markdown.yml@master integration_tests: if: "${{ github.event.inputs.PREP == '' && github.event.inputs.RELEASE == '' }}" - uses: optimizely/swift-sdk/.github/workflows/integration_tests.yml@yasir/gitAction + uses: optimizely/swift-sdk/.github/workflows/integration_tests.yml@master secrets: CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }} TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} @@ -47,7 +47,7 @@ jobs: unittests: if: "${{ github.event.inputs.PREP == '' && github.event.inputs.RELEASE == '' }}" - uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@yasir/gitAction + uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@master prepare_for_release: runs-on: macos-latest From 3db0ff4d6bec935e60bb4ff458dafe746d55bf86 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 10 May 2022 15:44:02 -0700 Subject: [PATCH 33/84] fix tests for default segments --- .../OptimizelyUserContextTests_Segments.swift | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index dfecc89f..c33c05a8 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -177,7 +177,7 @@ extension OptimizelyUserContextTests_Segments { XCTAssertEqual("https://api.zaius.com", optimizely.config?.hostForODP, "apiHost from datafile should be used as a default") XCTAssertEqual("$opt_user_id", segmentHandler.userKey, "the reserved user-key should be used as a default") XCTAssertEqual(kUserId, segmentHandler.userValue, "userId should be used as a default") - XCTAssertEqual(nil, segmentHandler.segmentsToCheck, "segmentsToCheck should be nil as a default") + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!), "segmentsToCheck should be all-in-project by default") XCTAssertEqual([], segmentHandler.options) } @@ -268,13 +268,12 @@ class MockAudienceSegmentsHandler: AudienceSegmentsHandler { // TODO: this test can be flaky. replace it with a good test account or remove it later. extension OptimizelyUserContextTests_Segments { - + var testODPApiHost: String { return "https://api.zaius.com" } + var testODPApiKeyForAudienceSegments: String { return "W4WzcEs-ABgXorzY7h1LCQ" } + var testODPUserKey: String { return "vuid" } + var testODPUserValue: String { return "d66a9d81923d4d2f99d8f64338976322" } + func testLiveODPGraphQL() { - let testODPApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" - let testODPUserValue = "d66a9d81923d4d2f99d8f64338976322" - let testODPUserKey = "vuid" - let testODPApiHost = "https://api.zaius.com" - let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) let user = optimizely.createUserContext(userId: kUserId) @@ -285,16 +284,13 @@ extension OptimizelyUserContextTests_Segments { userKey: testODPUserKey, userValue: testODPUserValue) { segments, error in XCTAssertNil(error) - XCTAssert(segments!.contains("has_email")) + 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() { - let testODPUserKey = "vuid" - let testODPUserValue = "d66a9d81923d4d2f99d8f64338976322" - let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) let user = optimizely.createUserContext(userId: kUserId) @@ -303,10 +299,26 @@ extension OptimizelyUserContextTests_Segments { user.fetchQualifiedSegments(userKey: testODPUserKey, userValue: testODPUserValue) { segments, error in XCTAssertNil(error) - XCTAssert(segments!.contains("has_email")) + XCTAssertEqual([], segments, "none of the test segments in the live ODP server") sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } + func testLiveODPGraphQL_noDatafile() { + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + let user = optimizely.createUserContext(userId: kUserId) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: testODPApiKeyForAudienceSegments, + apiHost: testODPApiHost, + userKey: testODPUserKey, + userValue: testODPUserValue) { segments, error in + XCTAssertNil(error) + XCTAssert(segments!.contains("has_email"), "test segments are not passed to ODP, so fetching all segments.") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + } From f329a2af9ae8af4f65d8e1c41dc4f72a94083883 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 11 May 2022 17:16:11 -0700 Subject: [PATCH 34/84] fix odp rest api error --- DemoSwiftApp/AppDelegate.swift | 5 +++-- Sources/ODP/ODPManager.swift | 33 ++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 4bc45ca0..16411734 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -142,10 +142,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("ODP register done") - if odp.isUserRegistered(userId: self.userId) { + let testOdpUser = "user-12345" + if odp.isUserRegistered(userId: testOdpUser) { print("ODP identify already done - skip identify sync step") } else { - odp.identify(userId: self.userId) { success in + odp.identify(userId: testOdpUser) { success in guard success else { print("ODP identify failed") return diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index 148aa3e8..caa3aa29 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -110,7 +110,7 @@ public class ODPManager { let combinedData: [String: Any] = [ "type": kinds[0], "action": kinds[1], - "data_source": "fullstack:swift-sdk", + //"data_source": "fullstack:swift-sdk", "identifiers": identifiers, "data": data ] @@ -124,16 +124,27 @@ public class ODPManager { request.addValue(odpPublicKey, forHTTPHeaderField: "x-api-key") request.addValue("application/json", forHTTPHeaderField: "content-type") -// let task = URLSession.shared.dataTask(with: url) { data, response, error in -// if let error = error { -// print("[ODP Event] API error: \(error)") -// completion(false) -// return -// } -// -// completion(true) -// } -// task.resume() + print("[ODP] request body: \(combinedData)") + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + print("[ODP Event] API error: \(error)") + completion(false) + return + } + + if let response = response as? HTTPURLResponse { + if response.statusCode >= 400 { + let message = data != nil ? String(bytes: data!, encoding: .utf8) : "UNKNOWN" + print("[ODP Event] API failed: \(message) \(self.odpPublicKey)") + completion(false) + return + } + } + + completion(true) + } + task.resume() completion(true) } From 3e8824903e2ab343571d0d6c856de3760b703eb1 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 12 May 2022 08:40:57 -0700 Subject: [PATCH 35/84] add email to odp profile --- Sources/ODP/ODPManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index caa3aa29..237bf393 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -89,6 +89,7 @@ public class ODPManager { if let userId = userId { identifiers["fs_user_id"] = userId + identifiers["email"] = "\(userId)@optimizely.com" } let kinds = kind.split(separator: ":") From 41b67ee69eb1833f6103a13d7ecb3efa501abd35 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 12 May 2022 10:48:52 -0700 Subject: [PATCH 36/84] remove vuid and fix fs_user_id based queries --- DemoSwiftApp/Samples/SamplesForAPI.swift | 2 +- .../OptimizelySegmentOption.swift | 2 + .../AudienceSegments/ZaiusApiManager.swift | 16 ++--- Sources/Optimizely/OptimizelyClient.swift | 2 +- Sources/Utils/Constants.swift | 2 +- .../OptimizelyUserContextTests_Segments.swift | 68 ++++++++++++++++--- .../ZaiusApiManagerTests.swift | 7 +- 7 files changed, 72 insertions(+), 27 deletions(-) diff --git a/DemoSwiftApp/Samples/SamplesForAPI.swift b/DemoSwiftApp/Samples/SamplesForAPI.swift index fcb3a13c..b04bbffb 100644 --- a/DemoSwiftApp/Samples/SamplesForAPI.swift +++ b/DemoSwiftApp/Samples/SamplesForAPI.swift @@ -276,7 +276,7 @@ class SamplesForAPI { } let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"]) - user.fetchQualifiedSegments(apiKey: "sample-api-key", options: [.ignoreCache]) { error in + user.fetchQualifiedSegments(apiKey: "sample-api-key", options: [.ignoreCache]) { _, error in guard error == nil else { print("[AudienceSegments] \(error!.errorDescription!)") return diff --git a/Sources/AudienceSegments/OptimizelySegmentOption.swift b/Sources/AudienceSegments/OptimizelySegmentOption.swift index dd186ca1..b0bdf37e 100644 --- a/Sources/AudienceSegments/OptimizelySegmentOption.swift +++ b/Sources/AudienceSegments/OptimizelySegmentOption.swift @@ -22,4 +22,6 @@ import Foundation case ignoreCache // reset cache case resetCache + // fetch all segments + case allSegments } diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 01143696..aaf00cc9 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -20,18 +20,17 @@ import Foundation // - https://api.zaius.com/v3/graphql // testODPApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" -// testODPUserIdForAudienceSegments = "d66a9d81923d4d2f99d8f64338976322" /* GraphQL Request // fetch all segments -curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql +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 {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql // fetch info for "has_email" segment only -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"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql +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"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql query MyQuery { - customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { + customer(fs_user_id: "tester-101") { audiences { edges { node { @@ -83,11 +82,6 @@ class ZaiusApiManager { userValue: String, segmentsToCheck: [String]?, completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - if userKey != "vuid" { - completionHandler(nil, .fetchSegmentsFailed("userKeys other than 'vuid' not supported yet")) - return - } - let subsetFilter = makeSubsetFilter(segments: segmentsToCheck) let body = [ @@ -128,9 +122,9 @@ class ZaiusApiManager { let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") else { self.logger.d { - "GraphQL decode failed: " + String(bytes: data, encoding: .utf8)! + "Segments not in GraphQL response JSON (the user may be not registered yet): " + String(bytes: data, encoding: .utf8)! } - completionHandler(nil, .fetchSegmentsFailed("decode error")) + completionHandler(nil, .fetchSegmentsFailed("segments not in json")) return } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 43131855..0849cd65 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -795,7 +795,7 @@ open class OptimizelyClient: NSObject { return } - let segmentsToCheck = config?.allSegments + let segmentsToCheck = options.contains(.allSegments) ? nil : config?.allSegments // filter out empty segmentsToCheck (segments not used in the project). // pass to zaius for non-empty segments or nil (to access all segments) only. diff --git a/Sources/Utils/Constants.swift b/Sources/Utils/Constants.swift index 2ccd6435..fbeea083 100644 --- a/Sources/Utils/Constants.swift +++ b/Sources/Utils/Constants.swift @@ -21,7 +21,7 @@ struct Constants { static let reservedBucketIdAttribute = "$opt_bucketing_id" static let reservedBotFilteringAttribute = "$opt_bot_filtering" static let reservedUserAgent = "$opt_user_agent" - static let reservedUserIdKey = "$opt_user_id" + static let reservedUserIdKey = "fs_user_id" } enum EvaluationLogType: String { diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index c33c05a8..7ed2b8d3 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -175,7 +175,7 @@ extension OptimizelyUserContextTests_Segments { XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", optimizely.config?.publicKeyForODP, "apiKey from datafile should be used as a default") XCTAssertEqual("https://api.zaius.com", optimizely.config?.hostForODP, "apiHost from datafile should be used as a default") - XCTAssertEqual("$opt_user_id", segmentHandler.userKey, "the reserved user-key should be used as a default") + XCTAssertEqual("fs_user_id", segmentHandler.userKey, "the reserved user-key should be used as a default") XCTAssertEqual(kUserId, segmentHandler.userValue, "userId should be used as a default") XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!), "segmentsToCheck should be all-in-project by default") XCTAssertEqual([], segmentHandler.options) @@ -270,13 +270,15 @@ class MockAudienceSegmentsHandler: AudienceSegmentsHandler { extension OptimizelyUserContextTests_Segments { var testODPApiHost: String { return "https://api.zaius.com" } var testODPApiKeyForAudienceSegments: String { return "W4WzcEs-ABgXorzY7h1LCQ" } + // {"vuid": "00TEST00VUID00FULLSTACK", "fs_user_id": "tester-101"} bound in ODP server for testing var testODPUserKey: String { return "vuid" } - var testODPUserValue: String { return "d66a9d81923d4d2f99d8f64338976322" } + var testODPUserValue: String { return "00TEST00VUID00FULLSTACK" } + var testODPUserId: String { return "tester-101"} func testLiveODPGraphQL() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) - let user = optimizely.createUserContext(userId: kUserId) + let user = optimizely.createUserContext(userId: testODPUserId) let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments(apiKey: testODPApiKeyForAudienceSegments, @@ -290,14 +292,32 @@ extension OptimizelyUserContextTests_Segments { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } + func testLiveODPGraphQL_allSegments() { + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + try! optimizely.start(datafile: datafile) + let user = optimizely.createUserContext(userId: testODPUserId) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(apiKey: testODPApiKeyForAudienceSegments, + apiHost: testODPApiHost, + userKey: testODPUserKey, + userValue: testODPUserValue, + options: [.allSegments]) { segments, error in + XCTAssertNil(error) + XCTAssert(segments!.contains("has_email"), "segmentsToCheck are not passed to ODP, so fetching all segments.") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + func testLiveODPGraphQL_defaultParameters() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) - let user = optimizely.createUserContext(userId: kUserId) + let user = optimizely.createUserContext(userId: testODPUserId) let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(userKey: testODPUserKey, - userValue: testODPUserValue) { segments, error in + user.fetchQualifiedSegments { segments, error in XCTAssertNil(error) XCTAssertEqual([], segments, "none of the test segments in the live ODP server") sem.signal() @@ -305,9 +325,23 @@ extension OptimizelyUserContextTests_Segments { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } + func testLiveODPGraphQL_defaultParameters_allSegments() { + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + try! optimizely.start(datafile: datafile) + let user = optimizely.createUserContext(userId: testODPUserId) + + let sem = DispatchSemaphore(value: 0) + user.fetchQualifiedSegments(options: [.allSegments]) { segments, error in + XCTAssertNil(error) + XCTAssert(segments!.contains("has_email"), "segmentsToCheck are not passed to ODP, so fetching all segments.") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + func testLiveODPGraphQL_noDatafile() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - let user = optimizely.createUserContext(userId: kUserId) + let user = optimizely.createUserContext(userId: testODPUserId) let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments(apiKey: testODPApiKeyForAudienceSegments, @@ -315,7 +349,25 @@ extension OptimizelyUserContextTests_Segments { userKey: testODPUserKey, userValue: testODPUserValue) { segments, error in XCTAssertNil(error) - XCTAssert(segments!.contains("has_email"), "test segments are not passed to ODP, so fetching all segments.") + XCTAssert(segments!.contains("has_email"), "segmentsToCheck are not passed to ODP, so fetching all segments.") + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + 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 .fetchSegmentsFailed("segments not in json") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift index e957cfd6..689c7356 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -18,10 +18,7 @@ import XCTest class ZaiusApiManagerTests: XCTestCase { - // TODO: currently "vuid" only supported - //var userKey = "test-user-key" - let userKey = "vuid" - + let userKey = "test-user-key" let userValue = "test-user-value" let apiKey = "test-api-key" let apiHost = "https://test-host" @@ -72,7 +69,7 @@ class ZaiusApiManagerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in - if case .fetchSegmentsFailed("decode error") = error { + if case .fetchSegmentsFailed("segments not in json") = error { XCTAssert(true) } else { XCTFail() From 13a59aed179267d84c418f4fa60bb40d2fa0124f Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 10 Jun 2022 08:45:57 -0700 Subject: [PATCH 37/84] adding vuid support --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 34 +++++ .../AudienceSegmentsHandler.swift | 48 +++++++ .../AudienceSegments/ZaiusApiManager.swift | 136 +++++++++++++++--- Sources/ODP/ODPManager.swift | 119 +++++++-------- Sources/ODP/VUIDManager.swift | 122 ++++++++++++++++ Sources/Optimizely/OptimizelyClient.swift | 24 ++++ 6 files changed, 401 insertions(+), 82 deletions(-) create mode 100644 Sources/ODP/VUIDManager.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 3cf86a1c..6516e82a 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1843,6 +1843,22 @@ 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 /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9432852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9442852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9452852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9462852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9472852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9482852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9492852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E94A2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E94B2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E94C2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E94D2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E94E2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E94F2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9502852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E9512852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.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 */; }; @@ -2289,6 +2305,7 @@ 84640880281320F000CCF97D /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.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 /* VUIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VUIDManager.swift; sourceTree = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_audience_segments.json; sourceTree = ""; }; @@ -2950,6 +2967,7 @@ isa = PBXGroup; children = ( 84037D4128257F6C00B33062 /* ODPManager.swift */, + 84E2E9412852A378001114AB /* VUIDManager.swift */, ); path = ODP; sourceTree = ""; @@ -4007,6 +4025,7 @@ 6E14CDA92423F9C300010234 /* Utils.swift in Sources */, 6EF8DE1F24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E14CD882423F9A100010234 /* AttributeValue.swift in Sources */, + 84E2E9492852A378001114AB /* VUIDManager.swift in Sources */, 6E14CD822423F9A100010234 /* DataStoreFile.swift in Sources */, 84037D4928257F6C00B33062 /* ODPManager.swift in Sources */, 6E14CDA42423F9C300010234 /* Notifications.swift in Sources */, @@ -4077,6 +4096,7 @@ 840583542808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E424D0226324B620081004A /* Audience.swift in Sources */, 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, + 84E2E9482852A378001114AB /* VUIDManager.swift in Sources */, 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */, 6E424D0526324B620081004A /* ConditionHolder.swift in Sources */, 6E424D5226324C4D0081004A /* OptimizelyClient+Decide.swift in Sources */, @@ -4180,6 +4200,7 @@ 6E75190122C520D500B2B157 /* Attribute.swift in Sources */, 6E6522BB278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, 6E7516B322C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 84E2E9432852A378001114AB /* VUIDManager.swift in Sources */, 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E7517D522C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75187122C520D400B2B157 /* Variation.swift in Sources */, @@ -4296,6 +4317,7 @@ 6E7518F022C520D500B2B157 /* ConditionHolder.swift in Sources */, 6EF8DE2424BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75183022C520D400B2B157 /* BatchEvent.swift in Sources */, + 84E2E94E2852A378001114AB /* VUIDManager.swift in Sources */, 6E75192022C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 84037D4E28257F6C00B33062 /* ODPManager.swift in Sources */, 6E9B117922C5487A00C22D81 /* tvOSOnlyTests.swift in Sources */, @@ -4366,6 +4388,7 @@ 6ECB60D7234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m in Sources */, 6E75195822C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E424C03263228FD0081004A /* AtomicDictionary.swift in Sources */, + 84E2E94A2852A378001114AB /* VUIDManager.swift in Sources */, 6E994B3A25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75170A22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E9B11AC22C5489300C22D81 /* OTUtils.swift in Sources */, @@ -4480,6 +4503,7 @@ 6E7518BF22C520D400B2B157 /* Variable.swift in Sources */, 6E75181722C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75185322C520D400B2B157 /* ProjectConfig.swift in Sources */, + 84E2E94D2852A378001114AB /* VUIDManager.swift in Sources */, 6E75173D22C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E7516E922C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7518A722C520D400B2B157 /* FeatureFlag.swift in Sources */, @@ -4628,6 +4652,7 @@ 6E9B117822C5487100C22D81 /* DataStoreTests.swift in Sources */, 6E75171B22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75195122C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, + 84E2E94F2852A378001114AB /* VUIDManager.swift in Sources */, 6E75176322C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E9B117722C5487100C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */, 6E7517DD22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, @@ -4699,6 +4724,7 @@ 6E75190A22C520D500B2B157 /* Attribute.swift in Sources */, 6E75171022C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C822C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 84E2E9502852A378001114AB /* VUIDManager.swift in Sources */, 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */, 6E75194622C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185622C520D400B2B157 /* ProjectConfig.swift in Sources */, @@ -4861,6 +4887,7 @@ 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */, + 84E2E9472852A378001114AB /* VUIDManager.swift in Sources */, 6E623F06253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E424BE2263228E90081004A /* AtomicArray.swift in Sources */, 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, @@ -4951,6 +4978,7 @@ 6E75190522C520D500B2B157 /* Attribute.swift in Sources */, 6E75170B22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C322C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 84E2E94B2852A378001114AB /* VUIDManager.swift in Sources */, 6E6522C3278DF27500954EA1 /* ZaiusApiManager.swift in Sources */, 6E75194122C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185122C520D400B2B157 /* ProjectConfig.swift in Sources */, @@ -5077,6 +5105,7 @@ 6E7517E622C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171822C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174822C520D400B2B157 /* HandlerRegistryService.swift in Sources */, + 84E2E94C2852A378001114AB /* VUIDManager.swift in Sources */, 6E7518FA22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7516E822C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75191222C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, @@ -5173,6 +5202,7 @@ 6E7517EB22C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171D22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174D22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, + 84E2E9512852A378001114AB /* VUIDManager.swift in Sources */, 6E7518FF22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7516ED22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75191722C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, @@ -5267,6 +5297,7 @@ 6E75195422C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E6522BA278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, 6E75171E22C520D400B2B157 /* OptimizelyResult.swift in Sources */, + 84E2E9422852A378001114AB /* VUIDManager.swift in Sources */, 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75189422C520D400B2B157 /* Experiment.swift in Sources */, @@ -5383,6 +5414,7 @@ 6E7518DE22C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6EF8DE1D24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7518EA22C520D400B2B157 /* ConditionHolder.swift in Sources */, + 84E2E9462852A378001114AB /* VUIDManager.swift in Sources */, 6E75182A22C520D400B2B157 /* BatchEvent.swift in Sources */, 84037D4628257F6C00B33062 /* ODPManager.swift in Sources */, 6E75191A22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, @@ -5465,6 +5497,7 @@ 75C71A2125E454460084187E /* EventForDispatch.swift in Sources */, 75C71A2225E454460084187E /* SemanticVersion.swift in Sources */, 75C71A2325E454460084187E /* Audience.swift in Sources */, + 84E2E9452852A378001114AB /* VUIDManager.swift in Sources */, 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, 75C71A2425E454460084187E /* AttributeValue.swift in Sources */, 75C71A2525E454460084187E /* ConditionLeaf.swift in Sources */, @@ -5541,6 +5574,7 @@ BD6485502491474500F30986 /* OPTBucketer.swift in Sources */, 6E6522BC278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, BD6485512491474500F30986 /* OptimizelyResult.swift in Sources */, + 84E2E9442852A378001114AB /* VUIDManager.swift in Sources */, BD6485522491474500F30986 /* Constants.swift in Sources */, BD6485532491474500F30986 /* DefaultLogger.swift in Sources */, BD6485542491474500F30986 /* Experiment.swift in Sources */, diff --git a/Sources/AudienceSegments/AudienceSegmentsHandler.swift b/Sources/AudienceSegments/AudienceSegmentsHandler.swift index da8823f5..7e662eb7 100644 --- a/Sources/AudienceSegments/AudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/AudienceSegmentsHandler.swift @@ -68,6 +68,54 @@ class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { } +// MARK: - VUID + +extension AudienceSegmentsHandler { + public func register(apiKey: String, + apiHost: String, + userKey: String, + userValue: String, + completion: ((Bool) -> Void)? = nil) { + let vuid = self.vuidManager.newVuid + + let identifiers = [ + "vuid": vuid + ] + + odpEvent(identifiers: identifiers, kind: "experimentation:client_initialized") { success in + if success { + print("[ODP] vuid registered (\(vuid)) successfully") + self.vuidManager.updateRegisteredVUID(vuid) + } + completion?(success) + } + } + + public func identify(apiKey: String, + apiHost: String, + userId: String, completion: ((Bool) -> Void)? = nil) { + guard let vuid = vuidManager.vuid else { + print("invalid vuid for identify") + return + } + + let identifiers = [ + "vuid": vuid, + "fs_user_id": userId + ] + + odpEvent(identifiers: identifiers, kind: "experimentation:identified") { success in + if success { + print("[ODP] add idenfier (\(userId)) successfully") + self.vuidManager.updateRegisteredUsers(userId: userId) + } + completion?(success) + } + } + + +} + // MARK: - Utils extension AudienceSegmentsHandler { diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 01143696..034e7020 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -16,36 +16,39 @@ import Foundation +// MARK: - GraphQL API + // ODP GraphQL API // - https://api.zaius.com/v3/graphql // testODPApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" // testODPUserIdForAudienceSegments = "d66a9d81923d4d2f99d8f64338976322" -/* GraphQL Request +/* -// fetch all segments -curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql - -// fetch info for "has_email" segment only -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"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql - -query MyQuery { - customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { - audiences { - edges { - node { - name - state - description - } - } - } - } -} -*/ + [GraphQL Request] + + // fetch all segments + curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql + + // fetch info for "has_email" segment only + 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"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql -/* GraphQL Response + query MyQuery { + customer(vuid: "d66a9d81923d4d2f99d8f64338976322") { + audiences { + edges { + node { + name + state + description + } + } + } + } + } + + [GraphQL Response] { "data": { @@ -72,7 +75,7 @@ query MyQuery { } } } - */ +*/ class ZaiusApiManager { let logger = OPTLoggerFactory.getLogger() @@ -166,6 +169,93 @@ class ZaiusApiManager { } +// MARK: - REST API + +extension ZaiusApiManager { + + public func odpEvent(apiKey: String, + apiHost: String, + identifiers: [String: Any], + kind: String, + data: [String: Any] = [:], + completion: @escaping (Bool) -> Void) { + let kinds = kind.split(separator: ":") + guard kinds.count == 2 else { + print("[ODP Event] invalid format for kind") + completion(false) + return + } + + var vuid = self.vuidManager.vuid + if vuid == nil { + vuid = self.vuidManager.newVuid + print("new vuid generated: \(vuid!)") + } + + var identifiers = [ + "vuid": vuid + ] + + if let userId = userId { + identifiers["fs_user_id"] = userId + } + + guard let url = URL(string: "\(apiHost)/v3/events") else { + print("[ODP Event] invalid url") + completion(false) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + + let combinedData: [String: Any] = [ + "type": kinds[0], + "action": kinds[1], + //"data_source": "fullstack:swift-sdk", + "identifiers": identifiers, + "data": data + ] + + guard let body = try? JSONSerialization.data(withJSONObject: combinedData) else { + print("[ODP Event] invalid JSON") + completion(false) + return + } + + request.httpBody = body + request.addValue(odpPublicKey, forHTTPHeaderField: "x-api-key") + request.addValue("application/json", forHTTPHeaderField: "content-type") + + print("[ODP] request body: \(combinedData)") + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + print("[ODP Event] API error: \(error)") + completion(false) + return + } + + if let response = response as? HTTPURLResponse { + if response.statusCode >= 400 { + let message = data != nil ? String(bytes: data!, encoding: .utf8) : "UNKNOWN" + print("[ODP Event] API failed: \(message) \(self.odpPublicKey)") + completion(false) + return + } + } + + completion(true) + } + task.resume() + + completion(true) + } + +} + +// MARK: - Utils + struct ODPAudience: Decodable { let name: String let state: String diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index 237bf393..9f8139e3 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -17,91 +17,91 @@ import Foundation public class ODPManager { - let keyForVuid = "vuid" - let keyForVuidUsers = "vuid-users" - - var odpPublicKey: String { - return "W4WzcEs-ABgXorzY7h1LCQ" - } - var odpHost: String { - return "https://api.zaius.com" - } - - public let vuid: String - var usersRegistered: Set - - init() { - if let vuid = UserDefaults.standard.string(forKey: keyForVuid) { - self.vuid = vuid - } else { - let vuid = UUID().uuidString.replacingOccurrences(of: "-", with: "") - print("vuid generated: \(vuid)") - UserDefaults.standard.set(vuid, forKey: keyForVuid) - self.vuid = vuid - } - - if let users = UserDefaults.standard.object(forKey: keyForVuidUsers) as? [String] { - self.usersRegistered = Set(users) - } else { - self.usersRegistered = [] - } - } + let vuidManager: VUIDManager - public func isUserRegistered(userId: String) -> Bool { - return usersRegistered.contains(userId) - } - - private func updateRegisteredUsers(userId: String) { - if usersRegistered.count > 10 { - usersRegistered = usersRegistered.filter { _ in Bool.random() } + init(vuidManager: VUIDManager? = nil) { + self.vuidManager = vuidManager ?? VUIDManager.shared + + if !self.vuidManager.isVUIDRegistered { + self.register() } - - usersRegistered.insert(userId) - UserDefaults.standard.set(Array(usersRegistered), forKey: keyForVuidUsers) - UserDefaults.standard.synchronize() - print("saved users: \(usersRegistered)") } - - public func register(completion: ((Bool) -> Void)? = nil) { - odpEvent(userId: nil, kind: "experimentation:client_initialized") { success in - if !success { - print("[ODP] register failed") + + public func register(apiKey: String? = nil, + apiHost: String? = nil, + completion: ((Bool) -> Void)? = nil) { + let vuid = self.vuidManager.newVuid + + let identifiers = [ + "vuid": vuid + ] + + odpEvent(identifiers: identifiers, kind: "experimentation:client_initialized") { success in + if success { + print("[ODP] vuid registered (\(vuid)) successfully") + self.vuidManager.updateRegisteredVUID(vuid) } completion?(success) } } - public func identify(userId: String, completion: ((Bool) -> Void)? = nil) { - odpEvent(userId: userId, kind: "experimentation:identified") { success in + public func identify(apiKey: String? = nil, + apiHost: String? = nil, + userId: String, completion: ((Bool) -> Void)? = nil) { + guard let vuid = vuidManager.vuid else { + print("invalid vuid for identify") + return + } + + let identifiers = [ + "vuid": vuid, + "fs_user_id": userId + ] + + odpEvent(identifiers: identifiers, kind: "experimentation:identified") { success in if success { print("[ODP] add idenfier (\(userId)) successfully") - - self.updateRegisteredUsers(userId: userId) + self.vuidManager.updateRegisteredUsers(userId: userId) } completion?(success) } } - public func odpEvent(userId: String?, kind: String, data: [String: Any] = [:], completion: @escaping (Bool) -> Void) { + public func odpEvent(identifiers: [String: Any], kind: String, data: [String: Any] = [:], completion: @escaping (Bool) -> Void) { + let kinds = kind.split(separator: ":") + guard kinds.count == 2 else { + print("[ODP Event] invalid format for kind") + completion(false) + return + } + + guard let url = URL(string: "\(odpHost)/v3/events") else { + print("[ODP Event] invalid url") + completion(false) + return + } + + var vuid = self.vuidManager.vuid + if vuid == nil { + vuid = self.vuidManager.newVuid + print("new vuid generated: \(vuid!)") + } + var identifiers = [ "vuid": vuid ] if let userId = userId { identifiers["fs_user_id"] = userId - identifiers["email"] = "\(userId)@optimizely.com" } - let kinds = kind.split(separator: ":") - guard kinds.count == 2 else { - print("[ODP Event] invalid format for kind") - completion(false) + guard let odpApiKey = apiKey ?? config?.publicKeyForODP else { + completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) return } - guard let url = URL(string: "https://api.zaius.com/v3/events") else { - print("[ODP Event] invalid url") - completion(false) + guard let odpApiHost = apiHost ?? config?.hostForODP else { + completionHandler(nil, .fetchSegmentsFailed("apiHost not defined")) return } @@ -121,6 +121,7 @@ public class ODPManager { completion(false) return } + request.httpBody = body request.addValue(odpPublicKey, forHTTPHeaderField: "x-api-key") request.addValue("application/json", forHTTPHeaderField: "content-type") diff --git a/Sources/ODP/VUIDManager.swift b/Sources/ODP/VUIDManager.swift new file mode 100644 index 00000000..b5d25148 --- /dev/null +++ b/Sources/ODP/VUIDManager.swift @@ -0,0 +1,122 @@ +// +// 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 VUIDManager { + static let shared = VUIDManager() + + var vuid: String? + var usersSet = Set() + var usersOrdered = [String]() + + let queue: DispatchQueue + + var newVuid: String { + return "VUID-" + UUID().uuidString.replacingOccurrences(of: "-", with: "") + } + + init() { + self.queue = DispatchQueue(label: "vuid") + + if let vuids = UserDefaults.standard.dictionary(forKey: keyForVuidMap) { + self.vuid = vuids[keyForVuid] as? String + + if let usersOrdered = vuids[keyForUsers] as? [String] { + self.usersOrdered = usersOrdered + self.usersSet = Set(usersOrdered) + } + } + } + + var isVUIDRegistered: Bool { + var registered = false + queue.sync { + registered = (self.vuid != nil) + } + return registered + } + + func updateRegisteredVUID(_ vuid: String) { + queue.async { + self.vuid = vuid + self.clearUsers() + } + } + + func isUserRegistered(userId: String) -> Bool { + var registered = false + queue.sync { + registered = usersSet.contains(userId) + } + return registered + } + + func updateRegisteredUsers(userId: String) { + queue.async { + if self.usersSet.contains(userId) { + return + } + + self.addUser(userId) + } + } + + func clearUsers() { + usersOrdered = [String]() + usersSet = Set() + saveVuidMap() + } + + func addUser(_ userId: String) { + if usersOrdered.count > 10 { + let first = usersOrdered.removeFirst() + usersSet.remove(first) + } + + usersSet.insert(userId) + usersOrdered.append(userId) + saveVuidMap() + } + +} + +// MARK: - UserDefaults + +extension VUIDManager { + + // UserDefaults format: (keep the most recent vuid info only) + // "optimizely-vuids": { + // "vuid": "vuid1", + // "users": ["userId1", "userId2"] + // } + var keyForVuidMap: String { return "optimizely-vuid-map" } + var keyForVuid: String { return "vuid" } + var keyForUsers: String { return "users" } + + func saveVuidMap() { + if let vuid = vuid { + let dict: [String: Any] = [keyForVuid: vuid, keyForUsers: usersOrdered] + UserDefaults.standard.set(dict, forKey: keyForVuidMap) + print("saved vuidMap: \(dict)") + } else { + UserDefaults.standard.removeObject(forKey: keyForVuidMap) + print("removed vuidMap") + } + UserDefaults.standard.synchronize() + } + +} diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 8bd0e56e..58a0d3f0 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -780,6 +780,30 @@ open class OptimizelyClient: NSObject { // MARK: - AudienceSegmentsHandler + func registerUser(apiKey: String?, + apiHost: String?, + userKey: String, + userValue: String, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + + guard let odpApiKey = apiKey ?? config?.publicKeyForODP else { + completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) + return + } + + guard let odpApiHost = apiHost ?? config?.hostForODP else { + completionHandler(nil, .fetchSegmentsFailed("apiHost not defined")) + return + } + + audienceSegmentsHandler.registerUser(apiKey: odpApiKey, + apiHost: odpApiHost, + userKey: userKey, + userValue: userValue, + completionHandler: completionHandler) + } + func fetchQualifiedSegments(apiKey: String?, apiHost: String?, userKey: String, From f6d05e709b634d600692d4e7e040a40f38ca5b59 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 10 Jun 2022 18:16:51 -0700 Subject: [PATCH 38/84] clean up vuid support --- DemoSwiftApp/AppDelegate.swift | 27 +-- DemoSwiftApp/Samples/SamplesForAPI.swift | 17 +- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 114 ++++--------- .../AudienceSegmentsHandler.swift | 130 +++++++++++---- .../OptimizelyODPConfig.swift | 38 +++++ .../VUIDManager.swift | 0 .../AudienceSegments/ZaiusApiManager.swift | 80 ++++----- .../OptimizelyClient+Extension.swift | 11 +- Sources/ODP/ODPManager.swift | 154 ------------------ .../OptimizelyUserContext.swift | 16 +- Sources/Optimizely/OptimizelyClient.swift | 115 +++++-------- Sources/Optimizely/OptimizelyError.swift | 4 +- .../Protocols/OPTAudienceSegmentHandler.swift | 29 ---- .../AudienceSegmentsHandlerTests.swift | 4 +- .../OptimizelyUserContextTests_Segments.swift | 14 +- 15 files changed, 278 insertions(+), 475 deletions(-) create mode 100644 Sources/AudienceSegments/OptimizelyODPConfig.swift rename Sources/{ODP => AudienceSegments}/VUIDManager.swift (100%) delete mode 100644 Sources/ODP/ODPManager.swift delete mode 100644 Sources/Protocols/OPTAudienceSegmentHandler.swift diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 16411734..6127db65 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -131,32 +131,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("Optimizely SDK initiliazation failed with unknown result") } - let odp = self.optimizely.odpManager - print("VUID: \(odp.vuid)") - - odp.register { success in - guard success else { - print("ODP register failed") - return - } - - print("ODP register done") - - let testOdpUser = "user-12345" - if odp.isUserRegistered(userId: testOdpUser) { - print("ODP identify already done - skip identify sync step") - } else { - odp.identify(userId: testOdpUser) { success in - guard success else { - print("ODP identify failed") - return - } - - print("ODP identify done") - } - } - - } + //self.startWithRootViewController() diff --git a/DemoSwiftApp/Samples/SamplesForAPI.swift b/DemoSwiftApp/Samples/SamplesForAPI.swift index b04bbffb..e283e470 100644 --- a/DemoSwiftApp/Samples/SamplesForAPI.swift +++ b/DemoSwiftApp/Samples/SamplesForAPI.swift @@ -147,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") @@ -168,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 @@ -267,8 +267,9 @@ class SamplesForAPI { // override the default handler if cache size and timeout need to be customized let optimizely = OptimizelyClient(sdkKey: "FCnSegiEkRry9rhVMroit4", periodicDownloadInterval: 60, - segmentsCacheSize: 12, - segmentsCacheTimeout: 123) + odpConfig: OptimizelyODPConfig(segmentsCacheSize: 12, + segmentsCacheTimeoutInSecs: 123, + apiKey: "sample-api-key")) optimizely.start { result in if case .failure(let error) = result { print("[AudienceSegments] SDK initialization failed: \(error)") @@ -276,7 +277,7 @@ class SamplesForAPI { } let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"]) - user.fetchQualifiedSegments(apiKey: "sample-api-key", options: [.ignoreCache]) { _, error in + user.fetchQualifiedSegments(options: [.ignoreCache]) { _, error in guard error == nil else { print("[AudienceSegments] \(error!.errorDescription!)") return diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 6516e82a..fa4e76e4 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1769,38 +1769,6 @@ 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 */; }; - 84037D4228257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4328257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4428257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4528257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4628257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4728257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4828257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4928257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4A28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4B28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4C28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4D28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4E28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D4F28257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D5028257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 84037D5128257F6C00B33062 /* ODPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84037D4128257F6C00B33062 /* ODPManager.swift */; }; - 8405834E2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 8405834F2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583502808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583512808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583522808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583532808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583542808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583552808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583562808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583572808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583582808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 840583592808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 8405835A2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 8405835B2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 8405835C2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; - 8405835D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */; }; 8428D3D02807337400D0FB0C /* LRUCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */; }; 8428D3D12807337400D0FB0C /* LRUCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */; }; 8428D3D5280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; @@ -1859,6 +1827,22 @@ 84E2E94F2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; 84E2E9502852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; 84E2E9512852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; + 84E2E96128540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96228540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96328540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96428540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96528540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96628540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96728540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96828540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96928540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96A28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96B28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96C28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96D28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96E28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E96F28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; + 84E2E97028540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.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 */; }; @@ -2296,8 +2280,6 @@ 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 = ""; }; - 84037D4128257F6C00B33062 /* ODPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ODPManager.swift; sourceTree = ""; }; - 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OPTAudienceSegmentHandler.swift; sourceTree = ""; }; 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCacheTests.swift; sourceTree = ""; }; 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiusApiManagerTests.swift; sourceTree = ""; }; 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandlerTests.swift; sourceTree = ""; }; @@ -2306,6 +2288,7 @@ 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 /* VUIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VUIDManager.swift; sourceTree = ""; }; + 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyODPConfig.swift; sourceTree = ""; }; 84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = ""; }; 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_audience_segments.json; sourceTree = ""; }; @@ -2523,8 +2506,10 @@ children = ( 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */, 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, - 6E6522FF278E688B00954EA1 /* LRUCache.swift */, 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, + 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */, + 84E2E9412852A378001114AB /* VUIDManager.swift */, + 6E6522FF278E688B00954EA1 /* LRUCache.swift */, ); path = AudienceSegments; sourceTree = ""; @@ -2541,7 +2526,6 @@ 6E75165D22C520D400B2B157 /* Sources */ = { isa = PBXGroup; children = ( - 84037D5228257F7500B33062 /* ODP */, 6E75166622C520D400B2B157 /* Optimizely */, 6EC6DD3F24ABF8180017D296 /* Optimizely+Decide */, 6E75165E22C520D400B2B157 /* Customization */, @@ -2722,7 +2706,6 @@ 6E7516A222C520D400B2B157 /* OPTDataStore.swift */, 6E7516A322C520D400B2B157 /* OPTDecisionService.swift */, 6E7516A522C520D400B2B157 /* OPTBucketer.swift */, - 8405834D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift */, ); path = Protocols; sourceTree = ""; @@ -2963,15 +2946,6 @@ path = watchOS; sourceTree = ""; }; - 84037D5228257F7500B33062 /* ODP */ = { - isa = PBXGroup; - children = ( - 84037D4128257F6C00B33062 /* ODPManager.swift */, - 84E2E9412852A378001114AB /* VUIDManager.swift */, - ); - path = ODP; - sourceTree = ""; - }; 87DE4DE091B80D1F13BBD781 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -3977,6 +3951,7 @@ 6E14CDA02423F9C300010234 /* OptimizelyClient+Extension.swift in Sources */, 6E14CDA22423F9C300010234 /* Array+Extension.swift in Sources */, 6E14CD952423F9A700010234 /* Group.swift in Sources */, + 84E2E96828540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E14CD9A2423F9C300010234 /* DataStoreQueueStack.swift in Sources */, 6E14CD732423F96F00010234 /* OptimizelyResult.swift in Sources */, 6E14CD7E2423F98D00010234 /* DefaultNotificationCenter.swift in Sources */, @@ -3991,7 +3966,6 @@ 6E14CD942423F9A700010234 /* FeatureFlag.swift in Sources */, 6E14CD9D2423F9C300010234 /* OPTDatafileHandler.swift in Sources */, 6E424BE3263228E90081004A /* AtomicArray.swift in Sources */, - 840583552808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3624BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E14CD7C2423F98D00010234 /* DefaultDatafileHandler.swift in Sources */, 6E14CD9B2423F9C300010234 /* OPTDataStore.swift in Sources */, @@ -4027,7 +4001,6 @@ 6E14CD882423F9A100010234 /* AttributeValue.swift in Sources */, 84E2E9492852A378001114AB /* VUIDManager.swift in Sources */, 6E14CD822423F9A100010234 /* DataStoreFile.swift in Sources */, - 84037D4928257F6C00B33062 /* ODPManager.swift in Sources */, 6E14CDA42423F9C300010234 /* Notifications.swift in Sources */, 6E20050B26B4D28500278087 /* MockLogger.swift in Sources */, 6E14CD872423F9A100010234 /* Audience.swift in Sources */, @@ -4079,6 +4052,7 @@ 6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */, 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */, + 84E2E96728540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */, 6E424CF626324B620081004A /* DefaultNotificationCenter.swift in Sources */, 6E424CF726324B620081004A /* DefaultDecisionService.swift in Sources */, @@ -4093,7 +4067,6 @@ 6E424CFF26324B620081004A /* BatchEvent.swift in Sources */, 6E424D0026324B620081004A /* EventForDispatch.swift in Sources */, 6E424D0126324B620081004A /* SemanticVersion.swift in Sources */, - 840583542808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E424D0226324B620081004A /* Audience.swift in Sources */, 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, 84E2E9482852A378001114AB /* VUIDManager.swift in Sources */, @@ -4123,7 +4096,6 @@ 6E424D5026324C4D0081004A /* OptimizelyDecideOption.swift in Sources */, 6E424D5126324C4D0081004A /* OptimizelyDecision.swift in Sources */, 6E424D1526324B620081004A /* DataStoreQueueStack.swift in Sources */, - 84037D4828257F6C00B33062 /* ODPManager.swift in Sources */, 6E424D1626324B620081004A /* OPTDataStore.swift in Sources */, 6E424D2F26324BBA0081004A /* OTUtils.swift in Sources */, 6E424D1726324B620081004A /* OPTDecisionService.swift in Sources */, @@ -4217,13 +4189,11 @@ 6E75172B22C520D400B2B157 /* Constants.swift in Sources */, 6E7516BF22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E424BDE263228E90081004A /* AtomicArray.swift in Sources */, - 8405834F2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E7517E122C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75178B22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E75177F22C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E75181122C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE1B24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, - 84037D4328257F6C00B33062 /* ODPManager.swift in Sources */, 6E75173722C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E7517F922C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, C78CAF592445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4247,6 +4217,7 @@ 6E7518D122C520D400B2B157 /* AttributeValue.swift in Sources */, 6E75182922C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75171F22C520D400B2B157 /* OptimizelyResult.swift in Sources */, + 84E2E96228540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E75170722C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75174322C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E75188922C520D400B2B157 /* Project.swift in Sources */, @@ -4269,6 +4240,7 @@ 6E75172622C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75173222C520D400B2B157 /* Constants.swift in Sources */, 6E75184822C520D400B2B157 /* Event.swift in Sources */, + 84E2E96D28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E75170E22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177A22C520D400B2B157 /* SDKVersion.swift in Sources */, 6E7516C622C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, @@ -4283,7 +4255,6 @@ 6E7518C022C520D400B2B157 /* Variable.swift in Sources */, 6E75181822C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E424BE8263228E90081004A /* AtomicArray.swift in Sources */, - 8405835A2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3B24BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75185422C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E9B11B422C5489500C22D81 /* OTUtils.swift in Sources */, @@ -4319,7 +4290,6 @@ 6E75183022C520D400B2B157 /* BatchEvent.swift in Sources */, 84E2E94E2852A378001114AB /* VUIDManager.swift in Sources */, 6E75192022C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, - 84037D4E28257F6C00B33062 /* ODPManager.swift in Sources */, 6E9B117922C5487A00C22D81 /* tvOSOnlyTests.swift in Sources */, 6E20051026B4D28500278087 /* MockLogger.swift in Sources */, 6E7518B422C520D400B2B157 /* Group.swift in Sources */, @@ -4376,7 +4346,6 @@ 6E75174622C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E75181422C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */, - 840583562808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E7516C222C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75188022C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E9B11DD22C548A200C22D81 /* OptimizelyClientTests_Valid.swift in Sources */, @@ -4416,7 +4385,6 @@ 6E5D12232638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6ECB60C6234D329500016D41 /* OptimizelyClientTests_OptimizelyConfig.swift in Sources */, 6EA2CC282345618E001E7531 /* OptimizelyConfig.swift in Sources */, - 84037D4A28257F6C00B33062 /* ODPManager.swift in Sources */, 6E75189822C520D400B2B157 /* Experiment.swift in Sources */, 6E8A3D4C2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 84B4D75827E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, @@ -4453,6 +4421,7 @@ 6E9B11AB22C5489300C22D81 /* MockUrlSession.swift in Sources */, 6E75190422C520D500B2B157 /* Attribute.swift in Sources */, 6E75193422C520D500B2B157 /* OPTDataStore.swift in Sources */, + 84E2E96928540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E75182C22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75175E22C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E9B11DE22C548A200C22D81 /* OptimizelyClientTests_Others.swift in Sources */, @@ -4475,6 +4444,7 @@ 6EC6DD4A24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75170122C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 8464087B28130D3200CCF97D /* Integration.swift in Sources */, + 84E2E96C28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6EF8DE3A24BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E7516B922C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E75175522C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4492,7 +4462,6 @@ 0B97DDA2249D4A25003DE606 /* SemanticVersion.swift in Sources */, 6E7516C522C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75189B22C520D400B2B157 /* Experiment.swift in Sources */, - 840583592808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75176122C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75180B22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, @@ -4538,7 +4507,6 @@ 6E75176D22C520D400B2B157 /* Utils.swift in Sources */, 6EA2CC2B2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E75182322C520D400B2B157 /* BatchEventBuilder.swift in Sources */, - 84037D4D28257F6C00B33062 /* ODPManager.swift in Sources */, 6EF8DE1524BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E424C06263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75174922C520D400B2B157 /* HandlerRegistryService.swift in Sources */, @@ -4586,7 +4554,6 @@ 0B97DDA4249D4A27003DE606 /* SemanticVersion.swift in Sources */, 6ECB60D3234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B116C22C5487100C22D81 /* BucketTests_ExpToVariation.swift in Sources */, - 84037D4F28257F6C00B33062 /* ODPManager.swift in Sources */, 6E7E9C4C25240D31009E4426 /* OptimizelyUserContextTests_Objc.m in Sources */, 6E75193922C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7518C122C520D400B2B157 /* Variable.swift in Sources */, @@ -4612,7 +4579,6 @@ 6E75190922C520D500B2B157 /* Attribute.swift in Sources */, 6E75177B22C520D400B2B157 /* SDKVersion.swift in Sources */, 84E7ABC827D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, - 8405835B2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E7E9B562523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */, 6EC6DD3C24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E75192D22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, @@ -4645,6 +4611,7 @@ 6E981FC3232C363300FADDD6 /* DecisionListenerTests_Datafile.swift in Sources */, 6E9B116422C5487100C22D81 /* BucketTests_Others.swift in Sources */, 6E7518CD22C520D400B2B157 /* Audience.swift in Sources */, + 84E2E96E28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E9B117322C5487100C22D81 /* BatchEventBuilderTests_Attributes.swift in Sources */, 6E9B11B622C5489600C22D81 /* OTUtils.swift in Sources */, 6E75183122C520D400B2B157 /* BatchEvent.swift in Sources */, @@ -4739,7 +4706,6 @@ 6E86CEAE24FDC84A005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E9B119922C5488300C22D81 /* UserAttributeTests_Evaluate.swift in Sources */, 6E9B11A422C5488300C22D81 /* ProjectTests.swift in Sources */, - 8405835C2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B119622C5488300C22D81 /* AudienceTests.swift in Sources */, 6E7518B622C520D400B2B157 /* Group.swift in Sources */, 6E7516D422C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4758,7 +4724,6 @@ 6E7516EC22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75181A22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE2624BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, - 84037D5028257F6C00B33062 /* ODPManager.swift in Sources */, 8464087E28130D3200CCF97D /* Integration.swift in Sources */, 6E9B119722C5488300C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184A22C520D400B2B157 /* Event.swift in Sources */, @@ -4783,6 +4748,7 @@ 6E7516BC22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E623F0E253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E4544B8270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 84E2E96F28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E7517A022C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517AC22C520D400B2B157 /* Array+Extension.swift in Sources */, 6EA425A52218E6AE00B074B5 /* (null) in Sources */, @@ -4893,7 +4859,6 @@ 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, 6E9B114F22C5486E00C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E7518F722C520D500B2B157 /* UserAttribute.swift in Sources */, - 840583532808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75174522C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E8A3D492637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E981FC2232C363300FADDD6 /* DecisionListenerTests_Datafile.swift in Sources */, @@ -4934,11 +4899,11 @@ 6E9B114D22C5486E00C22D81 /* BatchEventBuilderTests_Events.swift in Sources */, 6E75188B22C520D400B2B157 /* Project.swift in Sources */, 6E75187F22C520D400B2B157 /* TrafficAllocation.swift in Sources */, - 84037D4728257F6C00B33062 /* ODPManager.swift in Sources */, 6E0207A8272A11CF008C3711 /* NetworkReachabilityTests.swift in Sources */, 6E7518BB22C520D400B2B157 /* Variable.swift in Sources */, 6E7518AF22C520D400B2B157 /* Group.swift in Sources */, 6EF8DE3524BF7D69008B9488 /* DecisionReasons.swift in Sources */, + 84E2E96628540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E7517A522C520D400B2B157 /* Array+Extension.swift in Sources */, 6E9B115122C5486E00C22D81 /* NotificationCenterTests.swift in Sources */, 6E75184322C520D400B2B157 /* Event.swift in Sources */, @@ -4993,7 +4958,6 @@ 6E86CEA924FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E9B118322C5488100C22D81 /* UserAttributeTests_Evaluate.swift in Sources */, 6E9B118E22C5488100C22D81 /* ProjectTests.swift in Sources */, - 840583572808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E9B118022C5488100C22D81 /* AudienceTests.swift in Sources */, 6E7518B122C520D400B2B157 /* Group.swift in Sources */, 6E7516CF22C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -5012,7 +4976,6 @@ 6E7516E722C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75181522C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE2124BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, - 84037D4B28257F6C00B33062 /* ODPManager.swift in Sources */, 8464087928130D3200CCF97D /* Integration.swift in Sources */, 6E9B118122C5488100C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184522C520D400B2B157 /* Event.swift in Sources */, @@ -5037,6 +5000,7 @@ 6E7516B722C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E623F09253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E4544B3270E67C800F2CEBC /* NetworkReachability.swift in Sources */, + 84E2E96A28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E75179B22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517A722C520D400B2B157 /* Array+Extension.swift in Sources */, 6EA425962218E6AD00B074B5 /* (null) in Sources */, @@ -5079,6 +5043,7 @@ 6E75176C22C520D400B2B157 /* Utils.swift in Sources */, 6E7516C422C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, C78CAFAB24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, + 84E2E96B28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E86CEAA24FDC848005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75173022C520D400B2B157 /* Constants.swift in Sources */, 6E75181622C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, @@ -5121,7 +5086,6 @@ 6E4544B4270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75184622C520D400B2B157 /* Event.swift in Sources */, 6E7517CE22C520D400B2B157 /* DefaultBucketer.swift in Sources */, - 840583582808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75180A22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516B822C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C222C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, @@ -5141,7 +5105,6 @@ 6E9B11AF22C5489400C22D81 /* MockUrlSession.swift in Sources */, 6E7516F422C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75195A22C520D500B2B157 /* OPTBucketer.swift in Sources */, - 84037D4C28257F6C00B33062 /* ODPManager.swift in Sources */, 6E75177822C520D400B2B157 /* SDKVersion.swift in Sources */, 6E75194E22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75173C22C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5176,6 +5139,7 @@ 6E75177122C520D400B2B157 /* Utils.swift in Sources */, 6E7516C922C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, C78CAFB024486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, + 84E2E97028540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E86CEAF24FDC84B005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75173522C520D400B2B157 /* Constants.swift in Sources */, 6E75181B22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, @@ -5218,7 +5182,6 @@ 6E4544B9270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75184B22C520D400B2B157 /* Event.swift in Sources */, 6E7517D322C520D400B2B157 /* DefaultBucketer.swift in Sources */, - 8405835D2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75180F22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516BD22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C722C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, @@ -5238,7 +5201,6 @@ 6E9B11B922C5489700C22D81 /* MockUrlSession.swift in Sources */, 6E7516F922C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75195F22C520D500B2B157 /* OPTBucketer.swift in Sources */, - 84037D5128257F6C00B33062 /* ODPManager.swift in Sources */, 6E75177D22C520D400B2B157 /* SDKVersion.swift in Sources */, 6E75195322C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75174122C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5314,13 +5276,11 @@ 6E75194822C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7518E822C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E424BDD263228E90081004A /* AtomicArray.swift in Sources */, - 8405834E2808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6E75191822C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E7518B822C520D400B2B157 /* Variable.swift in Sources */, 6E75173622C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E75185822C520D400B2B157 /* FeatureVariable.swift in Sources */, 6EF8DE1A24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, - 84037D4228257F6C00B33062 /* ODPManager.swift in Sources */, 6E75186422C520D400B2B157 /* Rollout.swift in Sources */, 6E75179622C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, C78CAF582445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -5344,6 +5304,7 @@ 6E75193022C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7517E022C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E7516FA22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, + 84E2E96128540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E75187C22C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E7517EC22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E75174E22C520D400B2B157 /* LogMessage.swift in Sources */, @@ -5366,6 +5327,7 @@ 6E75172022C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75172C22C520D400B2B157 /* Constants.swift in Sources */, 6E75184222C520D400B2B157 /* Event.swift in Sources */, + 84E2E96528540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E75170822C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177422C520D400B2B157 /* SDKVersion.swift in Sources */, 6E7516C022C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, @@ -5380,7 +5342,6 @@ 6E7518BA22C520D400B2B157 /* Variable.swift in Sources */, 6E75181222C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E424BE1263228E90081004A /* AtomicArray.swift in Sources */, - 840583522808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 6EF8DE3424BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75184E22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E9B11A822C5489200C22D81 /* OTUtils.swift in Sources */, @@ -5416,7 +5377,6 @@ 6E7518EA22C520D400B2B157 /* ConditionHolder.swift in Sources */, 84E2E9462852A378001114AB /* VUIDManager.swift in Sources */, 6E75182A22C520D400B2B157 /* BatchEvent.swift in Sources */, - 84037D4628257F6C00B33062 /* ODPManager.swift in Sources */, 6E75191A22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E20050826B4D28500278087 /* MockLogger.swift in Sources */, 6E7518AE22C520D400B2B157 /* Group.swift in Sources */, @@ -5474,7 +5434,6 @@ 75C71A0D25E454460084187E /* OptimizelyUserContext+ObjC.swift in Sources */, 75C71A0E25E454460084187E /* DefaultLogger.swift in Sources */, 75C71A0F25E454460084187E /* DefaultUserProfileService.swift in Sources */, - 840583512808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, 75C71A1025E454460084187E /* DefaultEventDispatcher.swift in Sources */, 75C71A1125E454460084187E /* OPTLogger.swift in Sources */, 75C71A1225E454460084187E /* OPTUserProfileService.swift in Sources */, @@ -5489,6 +5448,7 @@ 75C71A1925E454460084187E /* DecisionReasons.swift in Sources */, 75C71A1A25E454460084187E /* DecisionResponse.swift in Sources */, 75C71A1B25E454460084187E /* DataStoreMemory.swift in Sources */, + 84E2E96428540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 75C71A1C25E454460084187E /* DataStoreUserDefaults.swift in Sources */, 75C71A1D25E454460084187E /* DataStoreFile.swift in Sources */, 75C71A1E25E454460084187E /* DataStoreQueueStackImpl.swift in Sources */, @@ -5526,7 +5486,6 @@ 75C71A3A25E454460084187E /* OPTBucketer.swift in Sources */, 75C71A3B25E454460084187E /* ArrayEventForDispatch+Extension.swift in Sources */, 75C71A3C25E454460084187E /* OptimizelyClient+Extension.swift in Sources */, - 84037D4528257F6C00B33062 /* ODPManager.swift in Sources */, 75C71A3D25E454460084187E /* DataStoreQueueStackImpl+Extension.swift in Sources */, 75C71A3E25E454460084187E /* Array+Extension.swift in Sources */, 75C71A3F25E454460084187E /* Constants.swift in Sources */, @@ -5591,13 +5550,11 @@ BD64855E2491474500F30986 /* OPTDatafileHandler.swift in Sources */, BD64855F2491474500F30986 /* ConditionHolder.swift in Sources */, 6E424BDF263228E90081004A /* AtomicArray.swift in Sources */, - 840583502808E0B9004B614D /* OPTAudienceSegmentHandler.swift in Sources */, BD6485602491474500F30986 /* OPTNotificationCenter.swift in Sources */, BD6485612491474500F30986 /* Variable.swift in Sources */, BD6485622491474500F30986 /* MurmurHash3.swift in Sources */, BD6485632491474500F30986 /* FeatureVariable.swift in Sources */, 6EF8DE1C24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, - 84037D4428257F6C00B33062 /* ODPManager.swift in Sources */, BD6485642491474500F30986 /* Rollout.swift in Sources */, BD6485652491474500F30986 /* DataStoreQueueStackImpl+Extension.swift in Sources */, BD6485662491474500F30986 /* OptimizelyJSON.swift in Sources */, @@ -5621,6 +5578,7 @@ BD6485742491474500F30986 /* OPTDataStore.swift in Sources */, BD6485752491474500F30986 /* DefaultDecisionService.swift in Sources */, BD6485762491474500F30986 /* OptimizelyLogLevel.swift in Sources */, + 84E2E96328540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, BD6485772491474500F30986 /* TrafficAllocation.swift in Sources */, BD6485782491474500F30986 /* DataStoreMemory.swift in Sources */, BD6485792491474500F30986 /* LogMessage.swift in Sources */, diff --git a/Sources/AudienceSegments/AudienceSegmentsHandler.swift b/Sources/AudienceSegments/AudienceSegmentsHandler.swift index 7e662eb7..d37b94b6 100644 --- a/Sources/AudienceSegments/AudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/AudienceSegmentsHandler.swift @@ -17,24 +17,41 @@ import Foundation import UIKit -class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { +class ODPManager { var zaiusMgr = ZaiusApiManager() var segmentsCache: LRUCache let logger = OPTLoggerFactory.getLogger() - // cache size and timeout can be customized by injecting a subclass - - init(cacheSize: Int, cacheTimeoutInSecs: Int) { - segmentsCache = LRUCache(size: cacheSize, timeoutInSecs: cacheTimeoutInSecs) + let odpConfig: OptimizelyODPConfig + let vuidManager: VUIDManager + + init(odpConfig: OptimizelyODPConfig, vuidManager: VUIDManager? = nil) { + self.odpConfig = odpConfig + self.segmentsCache = LRUCache(size: odpConfig.segmentsCacheSize, timeoutInSecs: odpConfig.segmentsCacheTimeoutInSecs) + self.vuidManager = vuidManager ?? VUIDManager.shared + + self.registerVUID(apiKey: nil, apiHost: nil) { error in + // stay silent for auto register on app start + } } - func fetchQualifiedSegments(apiKey: String, - apiHost: String, + func fetchQualifiedSegments(apiKey: String?, + apiHost: String?, userKey: String, userValue: String, segmentsToCheck: [String]? = nil, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + guard let odpApiKey = apiKey ?? odpConfig.apiKey else { + completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) + return + } + + guard let odpApiHost = apiHost ?? odpConfig.apiHost else { + completionHandler(nil, .fetchSegmentsFailed("apiHost not defined")) + return + } + let cacheKey = makeCacheKey(userKey, userValue) let ignoreCache = options.contains(.ignoreCache) @@ -51,11 +68,11 @@ class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { } } - zaiusMgr.fetch(apiKey: apiKey, - apiHost: apiHost, - userKey: userKey, - userValue: userValue, - segmentsToCheck: segmentsToCheck) { segments, err in + zaiusMgr.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) @@ -66,36 +83,71 @@ class AudienceSegmentsHandler: OPTAudienceSegmentsHandler { } } -} + // MARK: - VUID + + + // TODO: call this again after project config changed with a new datafile. + -// MARK: - VUID + public func registerVUID(apiKey: String?, + apiHost: String?, + completionHandler: @escaping (OptimizelyError?) -> Void) { + if isVUIDRegistered { + logger.d("ODP: vuid is registered already.") + completionHandler(nil) + return + } + + guard let odpApiKey = apiKey ?? odpConfig.apiKey else { + completionHandler(.odpEventFailed("apiKey not defined")) + return + } + + guard let odpApiHost = apiHost ?? odpConfig.apiHost else { + completionHandler(.odpEventFailed("apiHost not defined")) + return + } -extension AudienceSegmentsHandler { - public func register(apiKey: String, - apiHost: String, - userKey: String, - userValue: String, - completion: ((Bool) -> Void)? = nil) { let vuid = self.vuidManager.newVuid let identifiers = [ "vuid": vuid ] - - odpEvent(identifiers: identifiers, kind: "experimentation:client_initialized") { success in - if success { - print("[ODP] vuid registered (\(vuid)) successfully") + + zaiusMgr.sendODPEvent(apiKey: odpApiKey, + apiHost: odpApiHost, + identifiers: identifiers, + kind: "experimentation:client_initialized") { error in + if error == nil { + self.logger.d("ODP: vuid registered (\(vuid)) successfully") self.vuidManager.updateRegisteredVUID(vuid) } - completion?(success) + completionHandler(error) } } - public func identify(apiKey: String, - apiHost: String, - userId: String, completion: ((Bool) -> Void)? = nil) { + public func identifyUser(apiKey: String?, + apiHost: String?, + userId: String, + completionHandler: @escaping (OptimizelyError?) -> Void) { + if isUserRegistered(userId: userId) { + logger.d("ODP: user (\(userId)) is registered already.") + completionHandler(nil) + return + } + + guard let odpApiKey = apiKey ?? odpConfig.apiKey else { + completionHandler(.odpEventFailed("apiKey not defined")) + return + } + + guard let odpApiHost = apiHost ?? odpConfig.apiHost else { + completionHandler(.odpEventFailed("apiHost not defined")) + return + } + guard let vuid = vuidManager.vuid else { - print("invalid vuid for identify") + completionHandler(.odpEventFailed("invalid vuid for identify")) return } @@ -104,21 +156,31 @@ extension AudienceSegmentsHandler { "fs_user_id": userId ] - odpEvent(identifiers: identifiers, kind: "experimentation:identified") { success in - if success { - print("[ODP] add idenfier (\(userId)) successfully") + zaiusMgr.sendODPEvent(apiKey: odpApiKey, + apiHost: odpApiHost, + identifiers: identifiers, + kind: "experimentation:identified") { error in + if error == nil { + self.logger.d("ODP: idenfier (\(userId)) added successfully") self.vuidManager.updateRegisteredUsers(userId: userId) } - completion?(success) + completionHandler(error) } } + var isVUIDRegistered: Bool { + return vuidManager.isVUIDRegistered + } + + func isUserRegistered(userId: String) -> Bool { + return vuidManager.isUserRegistered(userId: userId) + } } // MARK: - Utils -extension AudienceSegmentsHandler { +extension ODPManager { func makeCacheKey(_ userKey: String, _ userValue: String) -> String { return userKey + "-$-" + userValue diff --git a/Sources/AudienceSegments/OptimizelyODPConfig.swift b/Sources/AudienceSegments/OptimizelyODPConfig.swift new file mode 100644 index 00000000..f5349bd4 --- /dev/null +++ b/Sources/AudienceSegments/OptimizelyODPConfig.swift @@ -0,0 +1,38 @@ +// +// 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 OptimizelyODPConfig { + /// maximum size (default = 100) of audience segments cache (optional) + let segmentsCacheSize: Int + /// timeout in seconds (default = 600) of audience segments cache (optional) + let segmentsCacheTimeoutInSecs: Int + /// The host URL for the ODP audience segments API (optional). If not provided, SDK will use the default host in datafile. + let apiHost: String? + /// The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile. + let apiKey: String? + + public init(segmentsCacheSize: Int = 100, + segmentsCacheTimeoutInSecs: Int = 600, + apiHost: String? = "https://api.zaius.com", + apiKey: String? = nil) { + self.segmentsCacheSize = segmentsCacheSize + self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs + self.apiHost = apiHost + self.apiKey = apiKey + } +} diff --git a/Sources/ODP/VUIDManager.swift b/Sources/AudienceSegments/VUIDManager.swift similarity index 100% rename from Sources/ODP/VUIDManager.swift rename to Sources/AudienceSegments/VUIDManager.swift diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/AudienceSegments/ZaiusApiManager.swift index 034e7020..06bd00c3 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/AudienceSegments/ZaiusApiManager.swift @@ -80,12 +80,12 @@ import Foundation class ZaiusApiManager { let logger = OPTLoggerFactory.getLogger() - func fetch(apiKey: String, - apiHost: String, - userKey: String, - userValue: String, - segmentsToCheck: [String]?, - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + func fetchSegments(apiKey: String, + apiHost: String, + userKey: String, + userValue: String, + segmentsToCheck: [String]?, + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { if userKey != "vuid" { completionHandler(nil, .fetchSegmentsFailed("userKeys other than 'vuid' not supported yet")) return @@ -173,42 +173,23 @@ class ZaiusApiManager { extension ZaiusApiManager { - public func odpEvent(apiKey: String, - apiHost: String, - identifiers: [String: Any], - kind: String, - data: [String: Any] = [:], - completion: @escaping (Bool) -> Void) { + public func sendODPEvent(apiKey: String, + apiHost: String, + identifiers: [String: Any], + kind: String, + data: [String: Any] = [:], + completionHandler: @escaping (OptimizelyError?) -> Void) { let kinds = kind.split(separator: ":") guard kinds.count == 2 else { - print("[ODP Event] invalid format for kind") - completion(false) + completionHandler(.odpEventFailed("Invalid format for kind")) return } - - var vuid = self.vuidManager.vuid - if vuid == nil { - vuid = self.vuidManager.newVuid - print("new vuid generated: \(vuid!)") - } - var identifiers = [ - "vuid": vuid - ] - - if let userId = userId { - identifiers["fs_user_id"] = userId - } - guard let url = URL(string: "\(apiHost)/v3/events") else { - print("[ODP Event] invalid url") - completion(false) + completionHandler(.odpEventFailed("Invalid url")) return } - var request = URLRequest(url: url) - request.httpMethod = "POST" - let combinedData: [String: Any] = [ "type": kinds[0], "action": kinds[1], @@ -218,38 +199,43 @@ extension ZaiusApiManager { ] guard let body = try? JSONSerialization.data(withJSONObject: combinedData) else { - print("[ODP Event] invalid JSON") - completion(false) + completionHandler(.odpEventFailed("Invalid JSON")) return } - request.httpBody = body - request.addValue(odpPublicKey, forHTTPHeaderField: "x-api-key") - request.addValue("application/json", forHTTPHeaderField: "content-type") + 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") print("[ODP] request body: \(combinedData)") - let task = URLSession.shared.dataTask(with: request) { data, response, error in + 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 if let error = error { - print("[ODP Event] API error: \(error)") - completion(false) + completionHandler(.odpEventFailed(error.localizedDescription)) return } if let response = response as? HTTPURLResponse { if response.statusCode >= 400 { - let message = data != nil ? String(bytes: data!, encoding: .utf8) : "UNKNOWN" - print("[ODP Event] API failed: \(message) \(self.odpPublicKey)") - completion(false) + var message = "UNKNOWN" + if let data = data, let msg = String(bytes: data, encoding: .utf8) { + message = msg + } + completionHandler(.odpEventFailed(message)) return } } - completion(true) + completionHandler(nil) } - task.resume() - completion(true) + task.resume() } } diff --git a/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index e840e9c3..738cf0a2 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -56,8 +56,7 @@ extension OptimizelyClient { /// Set this to 0 to disable periodic downloading. /// - defaultLogLevel: default log level (optional. default = .info) /// - defaultDecisionOptions: default decision optiopns (optional) - /// - segmentsCacheSize: maximum size (default = 100) of audience segments cache (optional) - /// - segmentsCacheTimeout: timeout in seconds (default = 600) of audience segments cache (optional) + /// - odpConfig: ODP configuration (optional) public convenience init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, @@ -66,9 +65,8 @@ extension OptimizelyClient { periodicDownloadInterval: Int?, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - segmentsCacheSize: Int? = nil, - segmentsCacheTimeout: Int? = nil) { - + odpConfig: OptimizelyODPConfig? = nil) { + self.init(sdkKey: sdkKey, logger: logger, eventDispatcher: eventDispatcher, @@ -76,8 +74,7 @@ extension OptimizelyClient { userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, defaultDecideOptions: defaultDecideOptions, - segmentsCacheSize: segmentsCacheSize, - segmentsCacheTimeout: segmentsCacheTimeout) + odpConfig: odpConfig) let interval = periodicDownloadInterval ?? 10 * 60 if interval > 0 { diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift deleted file mode 100644 index 9f8139e3..00000000 --- a/Sources/ODP/ODPManager.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// 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 class ODPManager { - let vuidManager: VUIDManager - - init(vuidManager: VUIDManager? = nil) { - self.vuidManager = vuidManager ?? VUIDManager.shared - - if !self.vuidManager.isVUIDRegistered { - self.register() - } - } - - public func register(apiKey: String? = nil, - apiHost: String? = nil, - completion: ((Bool) -> Void)? = nil) { - let vuid = self.vuidManager.newVuid - - let identifiers = [ - "vuid": vuid - ] - - odpEvent(identifiers: identifiers, kind: "experimentation:client_initialized") { success in - if success { - print("[ODP] vuid registered (\(vuid)) successfully") - self.vuidManager.updateRegisteredVUID(vuid) - } - completion?(success) - } - } - - public func identify(apiKey: String? = nil, - apiHost: String? = nil, - userId: String, completion: ((Bool) -> Void)? = nil) { - guard let vuid = vuidManager.vuid else { - print("invalid vuid for identify") - return - } - - let identifiers = [ - "vuid": vuid, - "fs_user_id": userId - ] - - odpEvent(identifiers: identifiers, kind: "experimentation:identified") { success in - if success { - print("[ODP] add idenfier (\(userId)) successfully") - self.vuidManager.updateRegisteredUsers(userId: userId) - } - completion?(success) - } - } - - public func odpEvent(identifiers: [String: Any], kind: String, data: [String: Any] = [:], completion: @escaping (Bool) -> Void) { - let kinds = kind.split(separator: ":") - guard kinds.count == 2 else { - print("[ODP Event] invalid format for kind") - completion(false) - return - } - - guard let url = URL(string: "\(odpHost)/v3/events") else { - print("[ODP Event] invalid url") - completion(false) - return - } - - var vuid = self.vuidManager.vuid - if vuid == nil { - vuid = self.vuidManager.newVuid - print("new vuid generated: \(vuid!)") - } - - var identifiers = [ - "vuid": vuid - ] - - if let userId = userId { - identifiers["fs_user_id"] = userId - } - - guard let odpApiKey = apiKey ?? config?.publicKeyForODP else { - completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) - return - } - - guard let odpApiHost = apiHost ?? config?.hostForODP else { - completionHandler(nil, .fetchSegmentsFailed("apiHost not defined")) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - - let combinedData: [String: Any] = [ - "type": kinds[0], - "action": kinds[1], - //"data_source": "fullstack:swift-sdk", - "identifiers": identifiers, - "data": data - ] - - guard let body = try? JSONSerialization.data(withJSONObject: combinedData) else { - print("[ODP Event] invalid JSON") - completion(false) - return - } - - request.httpBody = body - request.addValue(odpPublicKey, forHTTPHeaderField: "x-api-key") - request.addValue("application/json", forHTTPHeaderField: "content-type") - - print("[ODP] request body: \(combinedData)") - - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - print("[ODP Event] API error: \(error)") - completion(false) - return - } - - if let response = response as? HTTPURLResponse { - if response.statusCode >= 400 { - let message = data != nil ? String(bytes: data!, encoding: .utf8) : "UNKNOWN" - print("[ODP Event] API failed: \(message) \(self.odpPublicKey)") - completion(false) - return - } - } - - completion(true) - } - task.resume() - - completion(true) - } - -} diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 037fe3e6..0f366fcf 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -76,6 +76,12 @@ public class OptimizelyUserContext { self.atomicAttributes = AtomicProperty(property: attributes ?? [:], lock: lock) self.atomicForcedDecisions = AtomicProperty(property: nil, lock: lock) self.atomicQualifiedSegments = AtomicProperty(property: nil, lock: lock) + + self.optimizely?.registerUserToODP(userId: userId) { error in + if let error = error { + self.logger.e(error) + } + } } /// Sets an attribute for a given key. @@ -162,7 +168,7 @@ public class OptimizelyUserContext { } -// MARK: - AudienceSegments +// MARK: - ODP extension OptimizelyUserContext { @@ -178,9 +184,7 @@ extension OptimizelyUserContext { /// - userValue: The value of the user identifier (optional). /// - 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(apiKey: String? = nil, - apiHost: String? = nil, - userKey: String? = nil, + public func fetchQualifiedSegments(userKey: String? = nil, userValue: String? = nil, options: [OptimizelySegmentOption] = [], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { @@ -192,9 +196,7 @@ extension OptimizelyUserContext { let userKey = userKey ?? Constants.Attributes.reservedUserIdKey let userValue = userValue ?? userId - optimizely.fetchQualifiedSegments(apiKey: apiKey, - apiHost: apiHost, - userKey: userKey, + optimizely.fetchQualifiedSegments(userKey: userKey, userValue: userValue, options: options) { segments, err in guard err == nil, let segments = segments else { diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 58a0d3f0..809be9e5 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -54,26 +54,23 @@ open class OptimizelyClient: NSObject { var decisionService: OPTDecisionService! public var notificationCenter: OPTNotificationCenter? - var segmentsCacheSize = 100 - var segmentsCacheTimeoutInSecs = 600 - private var atomicAudienceSegmentsHandler = AtomicProperty() - var audienceSegmentsHandler: AudienceSegmentsHandler { + var odpConfig: OptimizelyODPConfig + private var atomicODPManager = AtomicProperty() + var odpManager: ODPManager { get { // instantiated on the first call (not instantiated when it's not used) - guard let handler = atomicAudienceSegmentsHandler.property else { - let defaultHandler = AudienceSegmentsHandler(cacheSize: segmentsCacheSize, cacheTimeoutInSecs: segmentsCacheTimeoutInSecs) - atomicAudienceSegmentsHandler.property = defaultHandler + guard let handler = atomicODPManager.property else { + let defaultHandler = ODPManager(odpConfig: odpConfig) + atomicODPManager.property = defaultHandler return defaultHandler } return handler } set { - atomicAudienceSegmentsHandler.property = newValue + atomicODPManager.property = newValue } } - public var odpManager = ODPManager() - // MARK: - Public interfaces /// OptimizelyClient init @@ -86,8 +83,7 @@ open class OptimizelyClient: NSObject { /// - userProfileService: custom UserProfileService (optional) /// - defaultLogLevel: default log level (optional. default = .info) /// - defaultDecisionOptions: default decision options (optional) - /// - segmentsCacheSize: maximum size (default = 100) of audience segments cache (optional) - /// - segmentsCacheTimeout: timeout in seconds (default = 600) of audience segments cache (optional) + /// - odpConfig: ODP configuration (optional) public init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, @@ -95,17 +91,11 @@ open class OptimizelyClient: NSObject { userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - segmentsCacheSize: Int? = nil, - segmentsCacheTimeout: Int? = nil) { + odpConfig: OptimizelyODPConfig? = nil) { self.sdkKey = sdkKey self.defaultDecideOptions = defaultDecideOptions ?? [] - if let segmentsCacheSize = segmentsCacheSize { - self.segmentsCacheSize = segmentsCacheSize - } - if let segmentsCacheTimeout = segmentsCacheTimeout { - self.segmentsCacheTimeoutInSecs = segmentsCacheTimeout - } + self.odpConfig = odpConfig ?? OptimizelyODPConfig() super.init() @@ -125,7 +115,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)") } @@ -778,60 +768,6 @@ open class OptimizelyClient: NSObject { return OptimizelyConfigImp(projectConfig: config) } - // MARK: - AudienceSegmentsHandler - - func registerUser(apiKey: String?, - apiHost: String?, - userKey: String, - userValue: String, - options: [OptimizelySegmentOption], - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - - guard let odpApiKey = apiKey ?? config?.publicKeyForODP else { - completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) - return - } - - guard let odpApiHost = apiHost ?? config?.hostForODP else { - completionHandler(nil, .fetchSegmentsFailed("apiHost not defined")) - return - } - - audienceSegmentsHandler.registerUser(apiKey: odpApiKey, - apiHost: odpApiHost, - userKey: userKey, - userValue: userValue, - completionHandler: completionHandler) - } - - func fetchQualifiedSegments(apiKey: String?, - apiHost: String?, - userKey: String, - userValue: String, - options: [OptimizelySegmentOption], - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - - guard let odpApiKey = apiKey ?? config?.publicKeyForODP else { - completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) - return - } - - guard let odpApiHost = apiHost ?? config?.hostForODP else { - completionHandler(nil, .fetchSegmentsFailed("apiHost not defined")) - return - } - - let segmentsToCheck = options.contains(.useSubset) ? config?.allSegments : nil - - audienceSegmentsHandler.fetchQualifiedSegments(apiKey: odpApiKey, - apiHost: odpApiHost, - userKey: userKey, - userValue: userValue, - segmentsToCheck: segmentsToCheck, - options: options, - completionHandler: completionHandler) - } - } // MARK: - Send Events @@ -995,6 +931,35 @@ extension OptimizelyClient { } +// MARK: - ODP + +extension OptimizelyClient { + + func registerUserToODP(userId: String, + completionHandler: @escaping (OptimizelyError?) -> Void) { + odpManager.identifyUser(apiKey: config?.publicKeyForODP, + apiHost: config?.hostForODP, + userId: userId, + completionHandler: completionHandler) + } + + func fetchQualifiedSegments(userKey: String, + userValue: String, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + let segmentsToCheck = options.contains(.useSubset) ? config?.allSegments : nil + + odpManager.fetchQualifiedSegments(apiKey: config?.publicKeyForODP, + apiHost: config?.hostForODP, + userKey: userKey, + userValue: userValue, + segmentsToCheck: segmentsToCheck, + options: options, + completionHandler: completionHandler) + } + +} + // MARK: - For test support extension OptimizelyClient { diff --git a/Sources/Optimizely/OptimizelyError.swift b/Sources/Optimizely/OptimizelyError.swift index 206f4382..0cc29e84 100644 --- a/Sources/Optimizely/OptimizelyError.swift +++ b/Sources/Optimizely/OptimizelyError.swift @@ -83,6 +83,7 @@ public enum OptimizelyError: Error { // MARK: - AudienceSegements Errors case fetchSegmentsFailed(_ hint: String) + case odpEventFailed(_ hint: String) } // MARK: - CustomStringConvertible @@ -152,7 +153,8 @@ extension OptimizelyError: CustomStringConvertible, ReasonProtocol { case .eventDispatchFailed(let hint): message = "Event dispatch failed (\(hint))." case .eventDispatcherConfigError(let hint): message = "EventDispatcher config error (\(hint))." - case .fetchSegmentsFailed(let hint): message = "Audience segments fetch failed (\(hint))." + case .fetchSegmentsFailed(let hint): message = "ODP: Audience segments fetch failed (\(hint))." + case .odpEventFailed(let hint): message = "ODP: Event send failed (\(hint))." } return message diff --git a/Sources/Protocols/OPTAudienceSegmentHandler.swift b/Sources/Protocols/OPTAudienceSegmentHandler.swift deleted file mode 100644 index 609c2e1f..00000000 --- a/Sources/Protocols/OPTAudienceSegmentHandler.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// 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 - -protocol OPTAudienceSegmentsHandler { - - func fetchQualifiedSegments(apiKey: String, - apiHost: String, - userKey: String, - userValue: String, - segmentsToCheck: [String]?, - options: [OptimizelySegmentOption], - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) - -} diff --git a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift index 154b4a04..92fdfbfa 100644 --- a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift @@ -16,8 +16,8 @@ import XCTest -class AudienceSegmentsHandlerTests: XCTestCase { - var handler = AudienceSegmentsHandler(cacheSize: 100, cacheTimeoutInSecs: 100) +class ODPManagerTests: XCTestCase { + var handler = ODPManager(odpConfig: OptimizelyODPConfig(segmentsCacheSize: 100, segmentsCacheTimeoutInSecs: 100)) var options = [OptimizelySegmentOption]() var apiKey = "valid" diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index a40ec8f8..834f7652 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -19,7 +19,7 @@ import XCTest class OptimizelyUserContextTests_Segments: XCTestCase { var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - var segmentHandler = MockAudienceSegmentsHandler(cacheSize: 100, cacheTimeoutInSecs: 100) + var odpManager = MockODPManager(cacheSize: 100, cacheTimeoutInSecs: 100) var user: OptimizelyUserContext! let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! @@ -30,7 +30,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { let kUserValue = "custom_id_value" override func setUp() { - optimizely.audienceSegmentsHandler = segmentHandler + optimizely.odpManager = odpManager user = optimizely.createUserContext(userId: kUserId) } @@ -138,14 +138,14 @@ class OptimizelyUserContextTests_Segments: XCTestCase { // MARK: - Customisze AudienceSegmentHandler - func testCustomizeAudienceSegmentsHandler() { + func testCustomizeODPManager() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, periodicDownloadInterval: 60, segmentsCacheSize: 12, segmentsCacheTimeout: 123) - XCTAssertEqual(12, optimizely.audienceSegmentsHandler.segmentsCache.size) - XCTAssertEqual(123, optimizely.audienceSegmentsHandler.segmentsCache.timeoutInSecs) + XCTAssertEqual(12, optimizely.odpManager.segmentsCache.size) + XCTAssertEqual(123, optimizely.odpManager.segmentsCache.timeoutInSecs) } } @@ -239,9 +239,9 @@ extension OptimizelyUserContextTests_Segments { } -// MARK: - MockAudienceSegmentsHandler +// MARK: - MockODPManager -class MockAudienceSegmentsHandler: AudienceSegmentsHandler { +class MockODPManager: ODPManager { var apiKey: String? var apiHost: String? var userKey: String? From dd5c82b72784a1fca17d62e35cade731923fc06f Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Sat, 11 Jun 2022 19:17:33 -0700 Subject: [PATCH 39/84] vuid local ready --- Sources/AudienceSegments/VUIDManager.swift | 202 +++++++++++++----- .../OptimizelyClient+Decide.swift | 6 + .../OptimizelyUserContext.swift | 9 +- 3 files changed, 158 insertions(+), 59 deletions(-) diff --git a/Sources/AudienceSegments/VUIDManager.swift b/Sources/AudienceSegments/VUIDManager.swift index b5d25148..96c9c0f6 100644 --- a/Sources/AudienceSegments/VUIDManager.swift +++ b/Sources/AudienceSegments/VUIDManager.swift @@ -19,104 +19,198 @@ import Foundation class VUIDManager { static let shared = VUIDManager() - var vuid: String? - var usersSet = Set() - var usersOrdered = [String]() - + var vuidMap: VUIDMap let queue: DispatchQueue - var newVuid: String { - return "VUID-" + UUID().uuidString.replacingOccurrences(of: "-", with: "") - } - init() { self.queue = DispatchQueue(label: "vuid") - - if let vuids = UserDefaults.standard.dictionary(forKey: keyForVuidMap) { - self.vuid = vuids[keyForVuid] as? String - - if let usersOrdered = vuids[keyForUsers] as? [String] { - self.usersOrdered = usersOrdered - self.usersSet = Set(usersOrdered) - } + self.vuidMap = VUIDMap() + } + + var vuid: String { + var vuid = "" + queue.sync { + vuid = self.vuidMap.vuid } + return vuid } var isVUIDRegistered: Bool { var registered = false queue.sync { - registered = (self.vuid != nil) + registered = self.vuidMap.registered } return registered } - func updateRegisteredVUID(_ vuid: String) { + func setVUIDRegistered() { queue.async { - self.vuid = vuid - self.clearUsers() + self.vuidMap.registered = true + self.vuidMap.save() } } func isUserRegistered(userId: String) -> Bool { var registered = false queue.sync { - registered = usersSet.contains(userId) + registered = self.vuidMap.usersSet.contains(userId) } return registered } - func updateRegisteredUsers(userId: String) { + func addRegisteredUser(userId: String) { queue.async { - if self.usersSet.contains(userId) { - return - } - - self.addUser(userId) + self.vuidMap.addUser(userId) + self.vuidMap.save() } } - func clearUsers() { - usersOrdered = [String]() - usersSet = Set() - saveVuidMap() - } +} + +// MARK: - ODP + +extension VUIDManager { - func addUser(_ userId: String) { - if usersOrdered.count > 10 { - let first = usersOrdered.removeFirst() - usersSet.remove(first) + public func registerVUID() { + if isVUIDRegistered { + logger.d("ODP: vuid is registered already.") + completionHandler(nil) + return + } + + guard let odpApiKey = apiKey ?? odpConfig.apiKey else { + completionHandler(.odpEventFailed("apiKey not defined")) + return } - usersSet.insert(userId) - usersOrdered.append(userId) - saveVuidMap() + guard let odpApiHost = apiHost ?? odpConfig.apiHost else { + completionHandler(.odpEventFailed("apiHost not defined")) + return + } + + let vuid = self.vuidManager.newVuid + + let identifiers = [ + "vuid": vuid + ] + + zaiusMgr.sendODPEvent(apiKey: odpApiKey, + apiHost: odpApiHost, + identifiers: identifiers, + kind: "experimentation:client_initialized") { error in + if error == nil { + self.logger.d("ODP: vuid registered (\(vuid)) successfully") + self.vuidManager.updateRegisteredVUID(vuid) + } + completionHandler(error) + } } + public func identifyUser(userId: String) { + if isUserRegistered(userId: userId) { + logger.d("ODP: user (\(userId)) is registered already.") + completionHandler(nil) + return + } + + guard let odpApiKey = apiKey ?? odpConfig.apiKey else { + completionHandler(.odpEventFailed("apiKey not defined")) + return + } + + guard let odpApiHost = apiHost ?? odpConfig.apiHost else { + completionHandler(.odpEventFailed("apiHost not defined")) + return + } + + guard let vuid = vuidManager.vuid else { + completionHandler(.odpEventFailed("invalid vuid for identify")) + return + } + + let identifiers = [ + "vuid": vuid, + "fs_user_id": userId + ] + + zaiusMgr.sendODPEvent(apiKey: odpApiKey, + apiHost: odpApiHost, + identifiers: identifiers, + kind: "experimentation:identified") { error in + if error == nil { + self.logger.d("ODP: idenfier (\(userId)) added successfully") + self.vuidManager.updateRegisteredUsers(userId: userId) + } + completionHandler(error) + } + } + } -// MARK: - UserDefaults +// MARK: - VUIDMap -extension VUIDManager { +struct VUIDMap { + var vuid: String = "" + var registered: Bool = false + var users = [String]() + var usersSet = Set() + + let maxUsersRegistered = 10 + + init() { + self.load() + } + + mutating func addUser(_ userId: String) { + if self.usersSet.contains(userId) { + return + } + + if users.count > maxUsersRegistered { + users.removeFirst() + } + users.append(userId) + + usersSet = Set(users) + save() + } + + // MARK: - UserDefaults // UserDefaults format: (keep the most recent vuid info only) // "optimizely-vuids": { // "vuid": "vuid1", + // "registered": true, // "users": ["userId1", "userId2"] // } - var keyForVuidMap: String { return "optimizely-vuid-map" } - var keyForVuid: String { return "vuid" } - var keyForUsers: String { return "users" } - - func saveVuidMap() { - if let vuid = vuid { - let dict: [String: Any] = [keyForVuid: vuid, keyForUsers: usersOrdered] - UserDefaults.standard.set(dict, forKey: keyForVuidMap) - print("saved vuidMap: \(dict)") - } else { - UserDefaults.standard.removeObject(forKey: keyForVuidMap) - print("removed vuidMap") + + var keyForVuidMap = "optimizely-vuid-map" + var keyForVuid = "vuid" + var keyForRegistered = "registered" + var keyForUsers = "users" + + mutating func load() { + guard let vuids = UserDefaults.standard.dictionary(forKey: keyForVuidMap), + let oldVuid = vuids[keyForVuid] as? String + else { + self.vuid = "VUID-" + UUID().uuidString.replacingOccurrences(of: "-", with: "") + self.registered = false + self.users = [] + self.usersSet = Set(users) + save() + return } + + self.vuid = oldVuid + self.registered = (vuids[keyForRegistered] as? Bool) ?? false + self.users = (vuids[keyForUsers] as? [String]) ?? [] + self.usersSet = Set(users) + } + + func save() { + let dict: [String: Any] = [keyForVuid: vuid, keyForRegistered: registered, keyForUsers: users] + UserDefaults.standard.set(dict, forKey: keyForVuidMap) + print("saved vuidMap: \(dict)") UserDefaults.standard.synchronize() } - } diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index 629afeb5..432ebaad 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -31,6 +31,12 @@ extension OptimizelyClient { return OptimizelyUserContext(optimizely: self, userId: userId, attributes: attributes) } + // VUID-based UserContext + public func createUserContext(attributes: [String: Any]? = nil) -> OptimizelyUserContext { + let vuid = odpManager.vuidManager.vuid + 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 0f366fcf..f6e0ccc4 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -178,24 +178,23 @@ extension OptimizelyUserContext { /// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time. /// /// - Parameters: - /// - apiKey: The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile. - /// - apiHost: The host URL for the ODP audience segments API (optional). If not provided, SDK will use the default host in datafile. /// - userKey: The name of the user identifier (optional). /// - userValue: The value of the user identifier (optional). /// - 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(userKey: String? = nil, - userValue: String? = nil, - options: [OptimizelySegmentOption] = [], + public func fetchQualifiedSegments(options: [OptimizelySegmentOption] = [], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { guard let optimizely = self.optimizely else { completionHandler(nil, .sdkNotReady) return } + + // VUID or USERID ??? let userKey = userKey ?? Constants.Attributes.reservedUserIdKey let userValue = userValue ?? userId + optimizely.fetchQualifiedSegments(userKey: userKey, userValue: userValue, options: options) { segments, err in From 7e90f5ad77702915d0bfca2fc8623c6bf099101c Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Sat, 11 Jun 2022 20:54:06 -0700 Subject: [PATCH 40/84] refactor odpManager --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 102 ++++++++++------ .../AudienceSegments/ODPEventManager.swift | 87 ++++++++++++++ ...SegmentsHandler.swift => ODPManager.swift} | 110 +++--------------- Sources/AudienceSegments/VUIDManager.swift | 84 +------------ .../OptimizelyUserContext.swift | 7 +- Sources/Optimizely/OptimizelyClient.swift | 8 +- 6 files changed, 180 insertions(+), 218 deletions(-) create mode 100644 Sources/AudienceSegments/ODPEventManager.swift rename Sources/AudienceSegments/{AudienceSegmentsHandler.swift => ODPManager.swift} (52%) diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index fa4e76e4..61151a7e 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -641,22 +641,22 @@ 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; 6E6522C9278DF27900954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522DE278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522DF278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E0278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E2278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E3278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E4278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E5278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E6278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E7278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E8278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522EA278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522EB278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522EC278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */; }; - 6E6522ED278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.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 */; }; @@ -1843,6 +1843,22 @@ 84E2E96E28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; 84E2E96F28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; 84E2E97028540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.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 */; }; @@ -2092,7 +2108,7 @@ 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 = ""; }; 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusApiManager.swift; sourceTree = ""; }; - 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandler.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 = ""; }; @@ -2289,6 +2305,7 @@ 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelySegmentOption.swift; sourceTree = ""; }; 84E2E9412852A378001114AB /* VUIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VUIDManager.swift; sourceTree = ""; }; 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyODPConfig.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_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_audience_segments.json; sourceTree = ""; }; @@ -2504,11 +2521,12 @@ 6E6522B8278DF20F00954EA1 /* AudienceSegments */ = { isa = PBXGroup; children = ( - 6E6522DD278E4F3800954EA1 /* AudienceSegmentsHandler.swift */, + 6E6522DD278E4F3800954EA1 /* ODPManager.swift */, 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */, 84E2E9412852A378001114AB /* VUIDManager.swift */, + 84E2E9712855875E001114AB /* ODPEventManager.swift */, 6E6522FF278E688B00954EA1 /* LRUCache.swift */, ); path = AudienceSegments; @@ -3982,7 +4000,7 @@ C78CAFA824486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E14CD932423F9A700010234 /* Experiment.swift in Sources */, 6E14CD982423F9C300010234 /* BackgroundingCallbacks.swift in Sources */, - 6E6522E5278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E5278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E623F07253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5C2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E14CDA52423F9C300010234 /* MurmurHash3.swift in Sources */, @@ -4016,6 +4034,7 @@ 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 */, @@ -4058,9 +4077,10 @@ 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 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E4278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */, 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */, 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */, @@ -4148,6 +4168,7 @@ 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 */, @@ -4213,7 +4234,7 @@ 6E75189522C520D400B2B157 /* Experiment.swift in Sources */, 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84B4D75127E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522DF278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522DF278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E7518D122C520D400B2B157 /* AttributeValue.swift in Sources */, 6E75182922C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75171F22C520D400B2B157 /* OptimizelyResult.swift in Sources */, @@ -4271,7 +4292,7 @@ C78CAFAD24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7516AE22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75195022C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 6E6522EA278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522EA278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E623F0C253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF612445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516D222C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4305,6 +4326,7 @@ 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 */, @@ -4365,6 +4387,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 */, @@ -4396,7 +4419,7 @@ 6E7517FC22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E75178222C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7518A422C520D400B2B157 /* FeatureFlag.swift in Sources */, - 6E6522E6278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E6278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E75185C22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E20050C26B4D28500278087 /* MockLogger.swift in Sources */, 6E75176A22C520D400B2B157 /* Utils.swift in Sources */, @@ -4464,7 +4487,7 @@ 6E75189B22C520D400B2B157 /* Experiment.swift in Sources */, 6E75176122C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75180B22C520D400B2B157 /* DataStoreFile.swift in Sources */, - 6E6522E9278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E9278E4F3800954EA1 /* ODPManager.swift in Sources */, 6EF8DE2324BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7517C322C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190722C520D500B2B157 /* Attribute.swift in Sources */, @@ -4527,6 +4550,7 @@ 6E75186B22C520D400B2B157 /* Rollout.swift in Sources */, 6E75183B22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75194322C520D500B2B157 /* OPTDecisionService.swift in Sources */, + 84E2E97D2855875E001114AB /* ODPEventManager.swift in Sources */, 6E75179122C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4640,11 +4664,12 @@ 6E9B116F22C5487100C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3F25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75174B22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 6E6522EB278E4F3800954EA1 /* AudienceSegmentsHandler.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 */, 6E75186D22C520D400B2B157 /* Rollout.swift in Sources */, @@ -4741,7 +4766,7 @@ 6E7518E622C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75179422C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11B722C5489600C22D81 /* MockUrlSession.swift in Sources */, - 6E6522EC278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522EC278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E9B119222C5488300C22D81 /* FeatureVariableTests.swift in Sources */, 6E75176422C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF632445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -4757,6 +4782,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 */, @@ -4843,7 +4869,7 @@ 6E75181322C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD6924AE94820017D296 /* OptimizelyUserContextTests.swift in Sources */, 6E75171522C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, - 6E6522E3278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E3278E4F3800954EA1 /* ODPManager.swift in Sources */, 6EA2CC272345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFA724486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E75185B22C520D400B2B157 /* FeatureVariable.swift in Sources */, @@ -4911,6 +4937,7 @@ 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; @@ -4993,7 +5020,7 @@ 6E7518E122C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75178F22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11AD22C5489300C22D81 /* MockUrlSession.swift in Sources */, - 6E6522E7278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E7278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E9B117C22C5488100C22D81 /* FeatureVariableTests.swift in Sources */, 6E75175F22C520D400B2B157 /* AtomicProperty.swift in Sources */, C78CAF5E2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, @@ -5009,6 +5036,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 */, @@ -5049,7 +5077,7 @@ 6E75181622C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75188E22C520D400B2B157 /* Project.swift in Sources */, 6E994B3C25A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 6E6522E8278E4F3800954EA1 /* AudienceSegmentsHandler.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 */, @@ -5112,6 +5140,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 */, @@ -5145,7 +5174,7 @@ 6E75181B22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75189322C520D400B2B157 /* Project.swift in Sources */, 6E994B4125A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 6E6522ED278E4F3800954EA1 /* AudienceSegmentsHandler.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 */, @@ -5208,6 +5237,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 */, @@ -5235,6 +5265,7 @@ 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 */, @@ -5300,7 +5331,7 @@ 6E75192422C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84B4D75027E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522DE278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522DE278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E75193022C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7517E022C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E7516FA22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -5358,7 +5389,7 @@ C78CAFA624486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7518D222C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516A822C520D400B2B157 /* DefaultLogger.swift in Sources */, - 6E6522E2278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E2278E4F3800954EA1 /* ODPManager.swift in Sources */, 6E623F05253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5A2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E75194A22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -5392,6 +5423,7 @@ 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 */, @@ -5440,6 +5472,7 @@ 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 */, @@ -5458,7 +5491,7 @@ 75C71A2225E454460084187E /* SemanticVersion.swift in Sources */, 75C71A2325E454460084187E /* Audience.swift in Sources */, 84E2E9452852A378001114AB /* VUIDManager.swift in Sources */, - 6E6522E1278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E1278E4F3800954EA1 /* ODPManager.swift in Sources */, 75C71A2425E454460084187E /* AttributeValue.swift in Sources */, 75C71A2525E454460084187E /* ConditionLeaf.swift in Sources */, 75C71A2625E454460084187E /* ConditionHolder.swift in Sources */, @@ -5509,6 +5542,7 @@ 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 */, @@ -5574,7 +5608,7 @@ BD6485722491474500F30986 /* DataStoreQueueStack.swift in Sources */, BD6485732491474500F30986 /* OptimizelyConfig.swift in Sources */, 84B4D75227E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522E0278E4F3800954EA1 /* AudienceSegmentsHandler.swift in Sources */, + 6E6522E0278E4F3800954EA1 /* ODPManager.swift in Sources */, BD6485742491474500F30986 /* OPTDataStore.swift in Sources */, BD6485752491474500F30986 /* DefaultDecisionService.swift in Sources */, BD6485762491474500F30986 /* OptimizelyLogLevel.swift in Sources */, diff --git a/Sources/AudienceSegments/ODPEventManager.swift b/Sources/AudienceSegments/ODPEventManager.swift new file mode 100644 index 00000000..982163d8 --- /dev/null +++ b/Sources/AudienceSegments/ODPEventManager.swift @@ -0,0 +1,87 @@ +// +// 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 { + let kind: String + let identifiers: [String: Any] + let data: [String: Any] +} + +class ODPEventManager { + var events: [ODPEvent] + let queue: DispatchQueue + + init() { + self.queue = DispatchQueue(label: "event") + self.events = [] + } +} + + +// MARK: - ODP + +extension ODPEventManager { + + func registerVUID(vuid: String) { + let identifiers = [ + "vuid": vuid + ] + + queue.async { + self.events.append(ODPEvent(kind: "experimentation:client_initialized", identifiers: identifiers, data: [:])) + } + } + + func identifyUser(vuid: String, userId: String) { + let identifiers = [ + "vuid": vuid, + "fs_user_id": userId + ] + + queue.async { + self.events.append(ODPEvent(kind: "experimentation:identified", identifiers: identifiers, data: [:])) + } + } + + func flush() { + var events = [ODPEvent]() + queue.sync { + events = self.events + } + + for event in events { + sendODPEvent(event) + } + } + + func sendODPEvent(_ event: ODPEvent) { + let odpApiKey: String = "" + let odpApiHost: String = "" + + zaiusMgr.sendODPEvent(apiKey: odpApiKey, + apiHost: odpApiHost, + identifiers: event.identifiers, + kind: event.kind, + data: event.data) { error in + // + } + } + +} + + diff --git a/Sources/AudienceSegments/AudienceSegmentsHandler.swift b/Sources/AudienceSegments/ODPManager.swift similarity index 52% rename from Sources/AudienceSegments/AudienceSegmentsHandler.swift rename to Sources/AudienceSegments/ODPManager.swift index d37b94b6..433785ef 100644 --- a/Sources/AudienceSegments/AudienceSegmentsHandler.swift +++ b/Sources/AudienceSegments/ODPManager.swift @@ -18,20 +18,23 @@ import Foundation import UIKit class ODPManager { + let odpConfig: OptimizelyODPConfig + var zaiusMgr = ZaiusApiManager() var segmentsCache: LRUCache - let logger = OPTLoggerFactory.getLogger() - - let odpConfig: OptimizelyODPConfig let vuidManager: VUIDManager - + var eventManager: ODPEventManager + + let logger = OPTLoggerFactory.getLogger() + init(odpConfig: OptimizelyODPConfig, vuidManager: VUIDManager? = nil) { self.odpConfig = odpConfig - self.segmentsCache = LRUCache(size: odpConfig.segmentsCacheSize, timeoutInSecs: odpConfig.segmentsCacheTimeoutInSecs) + self.eventManager = ODPEventManager() self.vuidManager = vuidManager ?? VUIDManager.shared + self.segmentsCache = LRUCache(size: odpConfig.segmentsCacheSize, timeoutInSecs: odpConfig.segmentsCacheTimeoutInSecs) - self.registerVUID(apiKey: nil, apiHost: nil) { error in - // stay silent for auto register on app start + if !self.vuidManager.isVUIDRegistered { + eventManager.registerVUID(vuid: self.vuidManager.vuid) } } @@ -83,99 +86,22 @@ class ODPManager { } } - // MARK: - VUID - - - // TODO: call this again after project config changed with a new datafile. - - - public func registerVUID(apiKey: String?, - apiHost: String?, - completionHandler: @escaping (OptimizelyError?) -> Void) { - if isVUIDRegistered { - logger.d("ODP: vuid is registered already.") - completionHandler(nil) - return - } - - guard let odpApiKey = apiKey ?? odpConfig.apiKey else { - completionHandler(.odpEventFailed("apiKey not defined")) - return - } - - guard let odpApiHost = apiHost ?? odpConfig.apiHost else { - completionHandler(.odpEventFailed("apiHost not defined")) - return - } - - let vuid = self.vuidManager.newVuid - - let identifiers = [ - "vuid": vuid - ] - - zaiusMgr.sendODPEvent(apiKey: odpApiKey, - apiHost: odpApiHost, - identifiers: identifiers, - kind: "experimentation:client_initialized") { error in - if error == nil { - self.logger.d("ODP: vuid registered (\(vuid)) successfully") - self.vuidManager.updateRegisteredVUID(vuid) - } - completionHandler(error) - } - } - - public func identifyUser(apiKey: String?, - apiHost: String?, - userId: String, - completionHandler: @escaping (OptimizelyError?) -> Void) { - if isUserRegistered(userId: userId) { + func identifyUser(userId: String) { + if vuidManager.isUserRegistered(userId: userId) { logger.d("ODP: user (\(userId)) is registered already.") - completionHandler(nil) - return - } - - guard let odpApiKey = apiKey ?? odpConfig.apiKey else { - completionHandler(.odpEventFailed("apiKey not defined")) - return - } - - guard let odpApiHost = apiHost ?? odpConfig.apiHost else { - completionHandler(.odpEventFailed("apiHost not defined")) - return - } - - guard let vuid = vuidManager.vuid else { - completionHandler(.odpEventFailed("invalid vuid for identify")) return } - - let identifiers = [ - "vuid": vuid, - "fs_user_id": userId - ] - zaiusMgr.sendODPEvent(apiKey: odpApiKey, - apiHost: odpApiHost, - identifiers: identifiers, - kind: "experimentation:identified") { error in - if error == nil { - self.logger.d("ODP: idenfier (\(userId)) added successfully") - self.vuidManager.updateRegisteredUsers(userId: userId) - } - completionHandler(error) - } + eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) } - var isVUIDRegistered: Bool { - return vuidManager.isVUIDRegistered + func updateODPConfig(apiKey: String?, apiHost: String?) { + // updaet apiKey for fetchQualifiedSegments + + // flush ODPEvents + eventManager.flush() } - func isUserRegistered(userId: String) -> Bool { - return vuidManager.isUserRegistered(userId: userId) - } - } // MARK: - Utils diff --git a/Sources/AudienceSegments/VUIDManager.swift b/Sources/AudienceSegments/VUIDManager.swift index 96c9c0f6..0b959621 100644 --- a/Sources/AudienceSegments/VUIDManager.swift +++ b/Sources/AudienceSegments/VUIDManager.swift @@ -21,7 +21,8 @@ class VUIDManager { var vuidMap: VUIDMap let queue: DispatchQueue - + let logger = OPTLoggerFactory.getLogger() + init() { self.queue = DispatchQueue(label: "vuid") self.vuidMap = VUIDMap() @@ -67,86 +68,6 @@ class VUIDManager { } -// MARK: - ODP - -extension VUIDManager { - - public func registerVUID() { - if isVUIDRegistered { - logger.d("ODP: vuid is registered already.") - completionHandler(nil) - return - } - - guard let odpApiKey = apiKey ?? odpConfig.apiKey else { - completionHandler(.odpEventFailed("apiKey not defined")) - return - } - - guard let odpApiHost = apiHost ?? odpConfig.apiHost else { - completionHandler(.odpEventFailed("apiHost not defined")) - return - } - - let vuid = self.vuidManager.newVuid - - let identifiers = [ - "vuid": vuid - ] - - zaiusMgr.sendODPEvent(apiKey: odpApiKey, - apiHost: odpApiHost, - identifiers: identifiers, - kind: "experimentation:client_initialized") { error in - if error == nil { - self.logger.d("ODP: vuid registered (\(vuid)) successfully") - self.vuidManager.updateRegisteredVUID(vuid) - } - completionHandler(error) - } - } - - public func identifyUser(userId: String) { - if isUserRegistered(userId: userId) { - logger.d("ODP: user (\(userId)) is registered already.") - completionHandler(nil) - return - } - - guard let odpApiKey = apiKey ?? odpConfig.apiKey else { - completionHandler(.odpEventFailed("apiKey not defined")) - return - } - - guard let odpApiHost = apiHost ?? odpConfig.apiHost else { - completionHandler(.odpEventFailed("apiHost not defined")) - return - } - - guard let vuid = vuidManager.vuid else { - completionHandler(.odpEventFailed("invalid vuid for identify")) - return - } - - let identifiers = [ - "vuid": vuid, - "fs_user_id": userId - ] - - zaiusMgr.sendODPEvent(apiKey: odpApiKey, - apiHost: odpApiHost, - identifiers: identifiers, - kind: "experimentation:identified") { error in - if error == nil { - self.logger.d("ODP: idenfier (\(userId)) added successfully") - self.vuidManager.updateRegisteredUsers(userId: userId) - } - completionHandler(error) - } - } - -} - // MARK: - VUIDMap struct VUIDMap { @@ -214,3 +135,4 @@ struct VUIDMap { UserDefaults.standard.synchronize() } } + diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index f6e0ccc4..08682a43 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -77,11 +77,8 @@ public class OptimizelyUserContext { self.atomicForcedDecisions = AtomicProperty(property: nil, lock: lock) self.atomicQualifiedSegments = AtomicProperty(property: nil, lock: lock) - self.optimizely?.registerUserToODP(userId: userId) { error in - if let error = error { - self.logger.e(error) - } - } + // USERID only (not VUID) + self.optimizely?.registerUserToODP(userId: userId) } /// Sets an attribute for a given key. diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 809be9e5..17d8205b 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -935,12 +935,8 @@ extension OptimizelyClient { extension OptimizelyClient { - func registerUserToODP(userId: String, - completionHandler: @escaping (OptimizelyError?) -> Void) { - odpManager.identifyUser(apiKey: config?.publicKeyForODP, - apiHost: config?.hostForODP, - userId: userId, - completionHandler: completionHandler) + func registerUserToODP(userId: String) { + odpManager.identifyUser(userId: userId) } func fetchQualifiedSegments(userKey: String, From e8101436f04cf29eea92117cae6df2888f6c804e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 23 Jun 2022 17:55:47 -0700 Subject: [PATCH 41/84] refact ODP modules --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 146 +++++++++++++----- .../{AudienceSegments => ODP}/LRUCache.swift | 0 .../ODPEventManager.swift | 49 +++--- Sources/ODP/ODPManager.swift | 72 +++++++++ .../ODPSegmentManager.swift} | 52 ++----- .../OptimizelyODPConfig.swift | 51 +++++- .../OptimizelySegmentOption.swift | 0 .../VUIDManager.swift | 0 .../ZaiusGraphQLApiManager.swift} | 73 +-------- Sources/ODP/ZaiusRestApiManager.swift | 92 +++++++++++ Sources/Optimizely/OptimizelyClient.swift | 26 +--- 11 files changed, 362 insertions(+), 199 deletions(-) rename Sources/{AudienceSegments => ODP}/LRUCache.swift (100%) rename Sources/{AudienceSegments => ODP}/ODPEventManager.swift (63%) create mode 100644 Sources/ODP/ODPManager.swift rename Sources/{AudienceSegments/ODPManager.swift => ODP/ODPSegmentManager.swift} (60%) rename Sources/{AudienceSegments => ODP}/OptimizelyODPConfig.swift (60%) rename Sources/{AudienceSegments => ODP}/OptimizelySegmentOption.swift (100%) rename Sources/{AudienceSegments => ODP}/VUIDManager.swift (100%) rename Sources/{AudienceSegments/ZaiusApiManager.swift => ODP/ZaiusGraphQLApiManager.swift} (73%) create mode 100644 Sources/ODP/ZaiusRestApiManager.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 61151a7e..f4faa2ff 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -625,22 +625,6 @@ 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 */; }; - 6E6522BA278DF26400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522BB278DF26400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522BC278DF26400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522BD278DF26400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522BE278DF27200954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522BF278DF27300954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C0278DF27300954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C1278DF27400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C2278DF27400954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C3278DF27500954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C4278DF27600954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C5278DF27600954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C6278DF27700954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */; }; - 6E6522C9278DF27900954EA1 /* ZaiusApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6522B9278DF26400954EA1 /* ZaiusApiManager.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 */; }; @@ -1793,6 +1777,54 @@ 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 */; }; + 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 /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617DB2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617DC2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617DD2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617DE2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617DF2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E02863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E12863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E22863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E32863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E42863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E52863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E62863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E72863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E82863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617E92863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; + 848617EA2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617EB2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617EC2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617ED2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617EE2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617EF2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F02863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F12863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F22863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F32863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F42863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F52863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F62863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F72863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F82863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; + 848617F92863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.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 */; }; @@ -2107,7 +2139,6 @@ 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 = ""; }; - 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusApiManager.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 = ""; }; @@ -2301,6 +2332,9 @@ 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandlerTests.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 = ""; }; + 848617C72863DC2700B7F41B /* ODPSegmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPSegmentManager.swift; sourceTree = ""; }; + 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusGraphQLApiManager.swift; sourceTree = ""; }; + 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusRestApiManager.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 /* VUIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VUIDManager.swift; sourceTree = ""; }; @@ -2518,18 +2552,20 @@ path = "OptimizelyTests-MultiClients"; sourceTree = ""; }; - 6E6522B8278DF20F00954EA1 /* AudienceSegments */ = { + 6E6522B8278DF20F00954EA1 /* ODP */ = { isa = PBXGroup; children = ( 6E6522DD278E4F3800954EA1 /* ODPManager.swift */, - 6E6522B9278DF26400954EA1 /* ZaiusApiManager.swift */, - 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, - 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */, 84E2E9412852A378001114AB /* VUIDManager.swift */, + 848617C72863DC2700B7F41B /* ODPSegmentManager.swift */, 84E2E9712855875E001114AB /* ODPEventManager.swift */, + 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, + 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */, 6E6522FF278E688B00954EA1 /* LRUCache.swift */, + 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */, + 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */, ); - path = AudienceSegments; + path = ODP; sourceTree = ""; }; 6E6BE008237F547200FE8274 /* optimizelyConfig */ = { @@ -2548,7 +2584,7 @@ 6EC6DD3F24ABF8180017D296 /* Optimizely+Decide */, 6E75165E22C520D400B2B157 /* Customization */, 6E75167C22C520D400B2B157 /* Implementation */, - 6E6522B8278DF20F00954EA1 /* AudienceSegments */, + 6E6522B8278DF20F00954EA1 /* ODP */, 6E75168822C520D400B2B157 /* Data Model */, 6E75169E22C520D400B2B157 /* Protocols */, 6E75167422C520D400B2B157 /* Extensions */, @@ -3964,10 +4000,12 @@ 6E14CDAB2423F9EB00010234 /* MockUrlSession.swift in Sources */, 6E14CDAA2423F9C300010234 /* SDKVersion.swift in Sources */, 6E14CD832423F9A100010234 /* DataStoreQueueStackImpl.swift in Sources */, + 848617F12863E21200B7F41B /* ZaiusRestApiManager.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 /* OptimizelyODPConfig.swift in Sources */, 6E14CD9A2423F9C300010234 /* DataStoreQueueStack.swift in Sources */, @@ -4024,6 +4062,7 @@ 6E14CD872423F9A100010234 /* Audience.swift in Sources */, 6E14CD7D2423F98D00010234 /* DefaultBucketer.swift in Sources */, 6E14CD792423F98D00010234 /* OPTLogger.swift in Sources */, + 848617E12863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E14CD992423F9C300010234 /* OPTNotificationCenter.swift in Sources */, 6E14CD722423F96B00010234 /* OptimizelyClient+ObjC.swift in Sources */, 6E14CDA62423F9C300010234 /* HandlerRegistryService.swift in Sources */, @@ -4041,7 +4080,6 @@ 6E14CD8A2423F9A100010234 /* ConditionHolder.swift in Sources */, 6E14CD9E2423F9C300010234 /* OPTBucketer.swift in Sources */, 6E14CD742423F97200010234 /* OptimizelyConfig.swift in Sources */, - 6E6522C1278DF27400954EA1 /* ZaiusApiManager.swift in Sources */, 6EF8DE1124BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E14CDA12423F9C300010234 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E994B3925A3E6EA00999262 /* DecisionResponse.swift in Sources */, @@ -4070,6 +4108,7 @@ 6E424CF426324B620081004A /* DecisionInfo.swift in Sources */, 6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */, 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, + 848617E02863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */, 84E2E96728540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */, @@ -4110,7 +4149,6 @@ 6E424D1026324B620081004A /* Group.swift in Sources */, 6E424D1126324B620081004A /* Variable.swift in Sources */, 6E424D1226324B620081004A /* Attribute.swift in Sources */, - 6E6522C0278DF27300954EA1 /* ZaiusApiManager.swift in Sources */, 6E424D1326324B620081004A /* BackgroundingCallbacks.swift in Sources */, 6E424D1426324B620081004A /* OPTNotificationCenter.swift in Sources */, 6E424D5026324C4D0081004A /* OptimizelyDecideOption.swift in Sources */, @@ -4144,6 +4182,8 @@ 6E424CBA26324B1D0081004A /* Notifications.swift in Sources */, 6E424CBB26324B1D0081004A /* MurmurHash3.swift in Sources */, 8464087628130D3200CCF97D /* Integration.swift in Sources */, + 848617CE2863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617F02863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E424CBC26324B1D0081004A /* HandlerRegistryService.swift in Sources */, 6E424CBD26324B1D0081004A /* LogMessage.swift in Sources */, 6E424CBE26324B1D0081004A /* AtomicProperty.swift in Sources */, @@ -4191,7 +4231,7 @@ 6E75186522C520D400B2B157 /* Rollout.swift in Sources */, 6E86CEA324FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75190122C520D500B2B157 /* Attribute.swift in Sources */, - 6E6522BB278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, + 848617DB2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E7516B322C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 84E2E9432852A378001114AB /* VUIDManager.swift in Sources */, 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, @@ -4207,6 +4247,7 @@ 6E75185922C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E4544AB270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E7517ED22C520D400B2B157 /* DataStoreMemory.swift in Sources */, + 848617EB2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E75172B22C520D400B2B157 /* Constants.swift in Sources */, 6E7516BF22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E424BDE263228E90081004A /* AtomicArray.swift in Sources */, @@ -4218,6 +4259,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 */, @@ -4256,10 +4298,12 @@ 6E75170222C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E7516BA22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E75175622C520D400B2B157 /* LogMessage.swift in Sources */, + 848617F62863E21200B7F41B /* ZaiusRestApiManager.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 /* OptimizelyODPConfig.swift in Sources */, 6E75170E22C520D400B2B157 /* OptimizelyClient.swift in Sources */, @@ -4316,6 +4360,7 @@ 6E7518B422C520D400B2B157 /* Group.swift in Sources */, 6E7518CC22C520D400B2B157 /* Audience.swift in Sources */, 6E75176E22C520D400B2B157 /* Utils.swift in Sources */, + 848617E62863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E75182422C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E75174A22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E7516F622C520D400B2B157 /* OptimizelyError.swift in Sources */, @@ -4333,7 +4378,6 @@ 6ECB60D2234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E75192C22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7517AA22C520D400B2B157 /* Array+Extension.swift in Sources */, - 6E6522C6278DF27700954EA1 /* ZaiusApiManager.swift in Sources */, 6EF8DE1624BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75186C22C520D400B2B157 /* Rollout.swift in Sources */, 6E994B3E25A3E6EA00999262 /* DecisionResponse.swift in Sources */, @@ -4369,6 +4413,7 @@ 6E75181422C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */, 6E7516C222C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 848617F22863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E75188022C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E9B11DD22C548A200C22D81 /* OptimizelyClientTests_Valid.swift in Sources */, 0B97DDA0249D4A24003DE606 /* SemanticVersion.swift in Sources */, @@ -4426,17 +4471,18 @@ 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 */, 6E75179A22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, - 6E6522C2278DF27400954EA1 /* ZaiusApiManager.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 /* ZaiusGraphQLApiManager.swift in Sources */, 6E7518E022C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E7518B022C520D400B2B157 /* Group.swift in Sources */, 6E75185022C520D400B2B157 /* ProjectConfig.swift in Sources */, @@ -4482,7 +4528,9 @@ 6E994B3D25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75170D22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177922C520D400B2B157 /* SDKVersion.swift in Sources */, + 848617F52863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 0B97DDA2249D4A25003DE606 /* SemanticVersion.swift in Sources */, + 848617E52863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E7516C522C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75189B22C520D400B2B157 /* Experiment.swift in Sources */, 6E75176122C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -4526,7 +4574,6 @@ 6E7518CB22C520D400B2B157 /* Audience.swift in Sources */, 6ECB60D1234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, C78CAFAC24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, - 6E6522C5278DF27600954EA1 /* ZaiusApiManager.swift in Sources */, 6E75176D22C520D400B2B157 /* Utils.swift in Sources */, 6EA2CC2B2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E75182322C520D400B2B157 /* BatchEventBuilder.swift in Sources */, @@ -4540,6 +4587,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 */, @@ -4561,13 +4609,13 @@ files = ( 6E7E9B382523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift in Sources */, 6E9B117022C5487100C22D81 /* DecisionListenerTests.swift in Sources */, + 848617F72863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E9B116122C5487100C22D81 /* OptimizelyErrorTests.swift in Sources */, 6E9B116522C5487100C22D81 /* BatchEventBuilderTest.swift in Sources */, 6E9B117422C5487100C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B116E22C5487100C22D81 /* LoggerTests.swift in Sources */, 84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */, 6E75180D22C520D400B2B157 /* DataStoreFile.swift in Sources */, - 6E6522C7278DF27700954EA1 /* ZaiusApiManager.swift in Sources */, 6E75178722C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E75179F22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7516BB22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, @@ -4662,6 +4710,7 @@ 6E75179322C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B117122C5487100C22D81 /* DecisionServiceTests_Features.swift in Sources */, 6E9B116F22C5487100C22D81 /* BucketTests_BucketVariation.swift in Sources */, + 848617E72863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E994B3F25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75174B22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E6522EB278E4F3800954EA1 /* ODPManager.swift in Sources */, @@ -4674,6 +4723,7 @@ 6E75181922C520D400B2B157 /* DataStoreQueueStackImpl.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 */, @@ -4717,7 +4767,6 @@ 6E75171022C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C822C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 84E2E9502852A378001114AB /* VUIDManager.swift in Sources */, - 6E6522C8278DF27800954EA1 /* ZaiusApiManager.swift in Sources */, 6E75194622C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185622C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20051226B4D28600278087 /* MockLogger.swift in Sources */, @@ -4743,6 +4792,7 @@ 6E7516E022C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E34A6212319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B119D22C5488300C22D81 /* UserAttributeTests.swift in Sources */, + 848617F82863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E5D12292638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75183E22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B11A022C5488300C22D81 /* ExperimentTests.swift in Sources */, @@ -4753,6 +4803,7 @@ 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 */, @@ -4800,6 +4851,7 @@ 6E75189222C520D400B2B157 /* Project.swift in Sources */, 6E7516F822C520D400B2B157 /* OptimizelyError.swift in Sources */, 84B4D75E27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 848617E82863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E424C09263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75189E22C520D400B2B157 /* Experiment.swift in Sources */, 6E75178822C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4875,7 +4927,6 @@ 6E75185B22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E7516B522C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7516A922C520D400B2B157 /* DefaultLogger.swift in Sources */, - 6E6522BF278DF27300954EA1 /* ZaiusApiManager.swift in Sources */, 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */, @@ -4895,10 +4946,12 @@ 6E7518D322C520D400B2B157 /* AttributeValue.swift in Sources */, 6E0A72D426C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift in Sources */, 6EF41A332522BE1900EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */, + 848617EF2863E21200B7F41B /* ZaiusRestApiManager.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 */, 6E75183722C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B115D22C5486E00C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */, @@ -4917,6 +4970,7 @@ 6E9B115722C5486E00C22D81 /* DecisionServiceTests_Features.swift in Sources */, 6E9B115522C5486E00C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3825A3E6EA00999262 /* DecisionResponse.swift in Sources */, + 848617DF2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E7516FD22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187322C520D400B2B157 /* Variation.swift in Sources */, 6E7517E322C520D400B2B157 /* DefaultDecisionService.swift in Sources */, @@ -4971,7 +5025,6 @@ 6E75170B22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C322C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 84E2E94B2852A378001114AB /* VUIDManager.swift in Sources */, - 6E6522C3278DF27500954EA1 /* ZaiusApiManager.swift in Sources */, 6E75194122C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185122C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20050D26B4D28500278087 /* MockLogger.swift in Sources */, @@ -4997,6 +5050,7 @@ 6E7516DB22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E34A61C2319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B118722C5488100C22D81 /* UserAttributeTests.swift in Sources */, + 848617F32863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E5D12242638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75183922C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B118A22C5488100C22D81 /* ExperimentTests.swift in Sources */, @@ -5007,6 +5061,7 @@ 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 */, @@ -5054,6 +5109,7 @@ 6E75188D22C520D400B2B157 /* Project.swift in Sources */, 6E7516F322C520D400B2B157 /* OptimizelyError.swift in Sources */, 84B4D75927E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, + 848617E32863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E424C04263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75189922C520D400B2B157 /* Experiment.swift in Sources */, 6E75178322C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -5089,6 +5145,7 @@ 6EC6DD4924ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75188222C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E5D12252638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, + 848617F42863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6ECB60D0234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B11E322C548AF00C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176022C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -5107,6 +5164,7 @@ 6E7516DC22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E75182222C520D400B2B157 /* BatchEventBuilder.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 */, @@ -5117,7 +5175,6 @@ 6E75180A22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516B822C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C222C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, - 6E6522C4278DF27600954EA1 /* ZaiusApiManager.swift in Sources */, 6E75191E22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75193622C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75170022C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -5128,6 +5185,7 @@ 84E7ABC527D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E7518BE22C520D400B2B157 /* Variable.swift in Sources */, 6E7518CA22C520D400B2B157 /* Audience.swift in Sources */, + 848617E42863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E75187622C520D400B2B157 /* Variation.swift in Sources */, 6E7517F222C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B11AF22C5489400C22D81 /* MockUrlSession.swift in Sources */, @@ -5186,6 +5244,7 @@ 6EC6DD4E24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75188722C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E5D122A2638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, + 848617F92863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6ECB60D5234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B11E522C548B100C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176522C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -5204,6 +5263,7 @@ 6E7516E122C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E75182722C520D400B2B157 /* BatchEventBuilder.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 */, @@ -5214,7 +5274,6 @@ 6E75180F22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7516BD22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E7517C722C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, - 6E6522C9278DF27900954EA1 /* ZaiusApiManager.swift in Sources */, 6E75192322C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E75193B22C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75170522C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, @@ -5225,6 +5284,7 @@ 84E7ABCA27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E7518C322C520D400B2B157 /* Variable.swift in Sources */, 6E7518CF22C520D400B2B157 /* Audience.swift in Sources */, + 848617E92863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E75187B22C520D400B2B157 /* Variation.swift in Sources */, 6E7517F722C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B11B922C5489700C22D81 /* MockUrlSession.swift in Sources */, @@ -5288,7 +5348,7 @@ 6E75180422C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E86CEA224FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75195422C520D500B2B157 /* OPTBucketer.swift in Sources */, - 6E6522BA278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, + 848617DA2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E75171E22C520D400B2B157 /* OptimizelyResult.swift in Sources */, 84E2E9422852A378001114AB /* VUIDManager.swift in Sources */, 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, @@ -5304,6 +5364,7 @@ 6E7517F822C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E4544AA270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E7517A222C520D400B2B157 /* Array+Extension.swift in Sources */, + 848617EA2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E75194822C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7518E822C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E424BDD263228E90081004A /* AtomicArray.swift in Sources */, @@ -5315,6 +5376,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 */, @@ -5353,10 +5415,12 @@ 6E7516FC22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E7516B422C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E75175022C520D400B2B157 /* LogMessage.swift in Sources */, + 848617EE2863E21200B7F41B /* ZaiusRestApiManager.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 /* OptimizelyODPConfig.swift in Sources */, 6E75170822C520D400B2B157 /* OptimizelyClient.swift in Sources */, @@ -5413,6 +5477,7 @@ 6E7518AE22C520D400B2B157 /* Group.swift in Sources */, 6E7518C622C520D400B2B157 /* Audience.swift in Sources */, 6E75176822C520D400B2B157 /* Utils.swift in Sources */, + 848617DE2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E75181E22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E75174422C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E7516F022C520D400B2B157 /* OptimizelyError.swift in Sources */, @@ -5430,7 +5495,6 @@ 6ECB60CC234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E75192622C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7517A422C520D400B2B157 /* Array+Extension.swift in Sources */, - 6E6522BE278DF27200954EA1 /* ZaiusApiManager.swift in Sources */, 6EF8DE0F24BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75186622C520D400B2B157 /* Rollout.swift in Sources */, 6E994B3725A3E6EA00999262 /* DecisionResponse.swift in Sources */, @@ -5468,6 +5532,7 @@ 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 */, @@ -5508,7 +5573,7 @@ 75C71A3025E454460084187E /* FeatureFlag.swift in Sources */, 75C71A3125E454460084187E /* Group.swift in Sources */, 75C71A3225E454460084187E /* Variable.swift in Sources */, - 6E6522BD278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, + 848617DD2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 75C71A3325E454460084187E /* Attribute.swift in Sources */, 75C71A3425E454460084187E /* BackgroundingCallbacks.swift in Sources */, 75C71A3525E454460084187E /* OPTNotificationCenter.swift in Sources */, @@ -5524,6 +5589,7 @@ 75C71A3F25E454460084187E /* Constants.swift in Sources */, 75C71A4025E454460084187E /* Notifications.swift in Sources */, 75C71A4125E454460084187E /* MurmurHash3.swift in Sources */, + 848617ED2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 75C71A4225E454460084187E /* HandlerRegistryService.swift in Sources */, 75C71A4325E454460084187E /* LogMessage.swift in Sources */, 75C71A4425E454460084187E /* AtomicProperty.swift in Sources */, @@ -5565,7 +5631,7 @@ BD64854F2491474500F30986 /* DataStoreFile.swift in Sources */, 6E86CEA424FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, BD6485502491474500F30986 /* OPTBucketer.swift in Sources */, - 6E6522BC278DF26400954EA1 /* ZaiusApiManager.swift in Sources */, + 848617DC2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, BD6485512491474500F30986 /* OptimizelyResult.swift in Sources */, 84E2E9442852A378001114AB /* VUIDManager.swift in Sources */, BD6485522491474500F30986 /* Constants.swift in Sources */, @@ -5581,6 +5647,7 @@ BD64855C2491474500F30986 /* DataStoreUserDefaults.swift in Sources */, 6E4544AC270E67C800F2CEBC /* NetworkReachability.swift in Sources */, BD64855D2491474500F30986 /* Array+Extension.swift in Sources */, + 848617EC2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, BD64855E2491474500F30986 /* OPTDatafileHandler.swift in Sources */, BD64855F2491474500F30986 /* ConditionHolder.swift in Sources */, 6E424BDF263228E90081004A /* AtomicArray.swift in Sources */, @@ -5592,6 +5659,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 */, diff --git a/Sources/AudienceSegments/LRUCache.swift b/Sources/ODP/LRUCache.swift similarity index 100% rename from Sources/AudienceSegments/LRUCache.swift rename to Sources/ODP/LRUCache.swift diff --git a/Sources/AudienceSegments/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift similarity index 63% rename from Sources/AudienceSegments/ODPEventManager.swift rename to Sources/ODP/ODPEventManager.swift index 982163d8..34dc12aa 100644 --- a/Sources/AudienceSegments/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -23,19 +23,21 @@ struct ODPEvent { } class ODPEventManager { + let odpConfig: OptimizelyODPConfig var events: [ODPEvent] let queue: DispatchQueue + let zaiusMgr: ZaiusRestApiManager + + let logger = OPTLoggerFactory.getLogger() - init() { - self.queue = DispatchQueue(label: "event") + init(odpConfig: OptimizelyODPConfig) { + self.odpConfig = odpConfig self.events = [] + self.queue = DispatchQueue(label: "event") + self.zaiusMgr = ZaiusRestApiManager() } -} - - -// MARK: - ODP - -extension ODPEventManager { + + // MARK: - ODP API func registerVUID(vuid: String) { let identifiers = [ @@ -44,6 +46,7 @@ extension ODPEventManager { queue.async { self.events.append(ODPEvent(kind: "experimentation:client_initialized", identifiers: identifiers, data: [:])) + self.flushEvents(self.events) } } @@ -55,30 +58,38 @@ extension ODPEventManager { queue.async { self.events.append(ODPEvent(kind: "experimentation:identified", identifiers: identifiers, data: [:])) + self.flushEvents(self.events) } } + // MARK: - Events + func flush() { - var events = [ODPEvent]() - queue.sync { - events = self.events + queue.async { + self.flushEvents(self.events) + } + } + + private func flushEvents(_ events: [ODPEvent]) { + guard let odpApiKey = odpConfig.apiKey else { + logger.d("ODP event cannot be dispatched since apiKey not defined") + return } for event in events { - sendODPEvent(event) + sendODPEvent(event, apiKey: odpApiKey, apiHost: odpConfig.apiHost) } } - func sendODPEvent(_ event: ODPEvent) { - let odpApiKey: String = "" - let odpApiHost: String = "" - - zaiusMgr.sendODPEvent(apiKey: odpApiKey, - apiHost: odpApiHost, + func sendODPEvent(_ event: ODPEvent, apiKey: String, apiHost: String) { + zaiusMgr.sendODPEvent(apiKey: apiKey, + apiHost: apiHost, identifiers: event.identifiers, kind: event.kind, data: event.data) { error in - // + if error != nil { + self.logger.w("ODP event dispatch failed: \(error!)") + } } } diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift new file mode 100644 index 00000000..4237d961 --- /dev/null +++ b/Sources/ODP/ODPManager.swift @@ -0,0 +1,72 @@ +// +// 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 { + let odpConfig: OptimizelyODPConfig + let vuidManager: VUIDManager + let segmentManager: ODPSegmentManager + let eventManager: ODPEventManager + + let logger = OPTLoggerFactory.getLogger() + + init(odpConfig: OptimizelyODPConfig, vuidManager: VUIDManager? = nil, segmentManager: ODPSegmentManager? = nil, eventManager: ODPEventManager? = nil) { + self.odpConfig = odpConfig + self.vuidManager = vuidManager ?? VUIDManager.shared + self.segmentManager = segmentManager ?? ODPSegmentManager(odpConfig: odpConfig) + self.eventManager = eventManager ?? ODPEventManager(odpConfig: odpConfig) + + if !self.vuidManager.isVUIDRegistered { + self.eventManager.registerVUID(vuid: self.vuidManager.vuid) + } + } + + func fetchQualifiedSegments(userKey: String, + userValue: String, + segmentsToCheck: [String]? = nil, + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + segmentManager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + segmentsToCheck: segmentsToCheck, + options: options, + completionHandler: completionHandler) + } + + func identifyUser(userId: String) { + if vuidManager.isUserRegistered(userId: userId) { + logger.d("ODP: user (\(userId)) is registered already.") + return + } + + eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) + } + + func updateODPConfig(apiKey: String?, apiHost: String?) { + guard let apiKey = apiKey, let apiHost = apiHost else { + logger.w("ODP: invalid apiKey or apiHost") + return + } + + odpConfig.apiKey = apiKey + odpConfig.apiHost = apiHost + + // flush all ODP events waiting for apiKey + eventManager.flush() + } + +} diff --git a/Sources/AudienceSegments/ODPManager.swift b/Sources/ODP/ODPSegmentManager.swift similarity index 60% rename from Sources/AudienceSegments/ODPManager.swift rename to Sources/ODP/ODPSegmentManager.swift index 433785ef..3b409b72 100644 --- a/Sources/AudienceSegments/ODPManager.swift +++ b/Sources/ODP/ODPSegmentManager.swift @@ -15,46 +15,30 @@ // import Foundation -import UIKit -class ODPManager { +class ODPSegmentManager { let odpConfig: OptimizelyODPConfig - - var zaiusMgr = ZaiusApiManager() - var segmentsCache: LRUCache - let vuidManager: VUIDManager - var eventManager: ODPEventManager + let zaiusMgr = ZaiusGraphQLApiManager() + let segmentsCache: LRUCache let logger = OPTLoggerFactory.getLogger() - init(odpConfig: OptimizelyODPConfig, vuidManager: VUIDManager? = nil) { + init(odpConfig: OptimizelyODPConfig) { self.odpConfig = odpConfig - self.eventManager = ODPEventManager() - self.vuidManager = vuidManager ?? VUIDManager.shared - self.segmentsCache = LRUCache(size: odpConfig.segmentsCacheSize, timeoutInSecs: odpConfig.segmentsCacheTimeoutInSecs) - - if !self.vuidManager.isVUIDRegistered { - eventManager.registerVUID(vuid: self.vuidManager.vuid) - } + self.segmentsCache = LRUCache(size: odpConfig.segmentsCacheSize, + timeoutInSecs: odpConfig.segmentsCacheTimeoutInSecs) } - func fetchQualifiedSegments(apiKey: String?, - apiHost: String?, - userKey: String, + func fetchQualifiedSegments(userKey: String, userValue: String, segmentsToCheck: [String]? = nil, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - guard let odpApiKey = apiKey ?? odpConfig.apiKey else { + guard let odpApiKey = odpConfig.apiKey else { completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) return } - guard let odpApiHost = apiHost ?? odpConfig.apiHost else { - completionHandler(nil, .fetchSegmentsFailed("apiHost not defined")) - return - } - let cacheKey = makeCacheKey(userKey, userValue) let ignoreCache = options.contains(.ignoreCache) @@ -72,7 +56,7 @@ class ODPManager { } zaiusMgr.fetchSegments(apiKey: odpApiKey, - apiHost: odpApiHost, + apiHost: odpConfig.apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: segmentsToCheck) { segments, err in @@ -86,27 +70,11 @@ class ODPManager { } } - func identifyUser(userId: String) { - if vuidManager.isUserRegistered(userId: userId) { - logger.d("ODP: user (\(userId)) is registered already.") - return - } - - eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) - } - - func updateODPConfig(apiKey: String?, apiHost: String?) { - // updaet apiKey for fetchQualifiedSegments - - // flush ODPEvents - eventManager.flush() - } - } // MARK: - Utils -extension ODPManager { +extension ODPSegmentManager { func makeCacheKey(_ userKey: String, _ userValue: String) -> String { return userKey + "-$-" + userValue diff --git a/Sources/AudienceSegments/OptimizelyODPConfig.swift b/Sources/ODP/OptimizelyODPConfig.swift similarity index 60% rename from Sources/AudienceSegments/OptimizelyODPConfig.swift rename to Sources/ODP/OptimizelyODPConfig.swift index f5349bd4..063576b4 100644 --- a/Sources/AudienceSegments/OptimizelyODPConfig.swift +++ b/Sources/ODP/OptimizelyODPConfig.swift @@ -16,23 +16,62 @@ import Foundation -public struct OptimizelyODPConfig { +public class OptimizelyODPConfig { /// maximum size (default = 100) of audience segments cache (optional) let segmentsCacheSize: Int /// timeout in seconds (default = 600) of audience segments cache (optional) let segmentsCacheTimeoutInSecs: Int /// The host URL for the ODP audience segments API (optional). If not provided, SDK will use the default host in datafile. - let apiHost: String? + private var _apiHost: String = "https://api.zaius.com" /// The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile. - let apiKey: String? + private var _apiKey: String? + + let queue = DispatchQueue(label: "odpConfig") public init(segmentsCacheSize: Int = 100, segmentsCacheTimeoutInSecs: Int = 600, - apiHost: String? = "https://api.zaius.com", + apiHost: String = "https://api.zaius.com", apiKey: String? = nil) { self.segmentsCacheSize = segmentsCacheSize self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs - self.apiHost = apiHost - self.apiKey = apiKey + self._apiHost = apiHost + self._apiKey = apiKey + } +} + +// MARK: - Thread-safe + +extension OptimizelyODPConfig { + + var apiHost: String { + get { + var value = "" + 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 + } + } } + } + diff --git a/Sources/AudienceSegments/OptimizelySegmentOption.swift b/Sources/ODP/OptimizelySegmentOption.swift similarity index 100% rename from Sources/AudienceSegments/OptimizelySegmentOption.swift rename to Sources/ODP/OptimizelySegmentOption.swift diff --git a/Sources/AudienceSegments/VUIDManager.swift b/Sources/ODP/VUIDManager.swift similarity index 100% rename from Sources/AudienceSegments/VUIDManager.swift rename to Sources/ODP/VUIDManager.swift diff --git a/Sources/AudienceSegments/ZaiusApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift similarity index 73% rename from Sources/AudienceSegments/ZaiusApiManager.swift rename to Sources/ODP/ZaiusGraphQLApiManager.swift index 06bd00c3..4e409c3e 100644 --- a/Sources/AudienceSegments/ZaiusApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -77,7 +77,7 @@ import Foundation } */ -class ZaiusApiManager { +class ZaiusGraphQLApiManager { let logger = OPTLoggerFactory.getLogger() func fetchSegments(apiKey: String, @@ -169,77 +169,6 @@ class ZaiusApiManager { } -// MARK: - REST API - -extension ZaiusApiManager { - - public func sendODPEvent(apiKey: String, - apiHost: String, - identifiers: [String: Any], - kind: String, - data: [String: Any] = [:], - completionHandler: @escaping (OptimizelyError?) -> Void) { - let kinds = kind.split(separator: ":") - guard kinds.count == 2 else { - completionHandler(.odpEventFailed("Invalid format for kind")) - return - } - - guard let url = URL(string: "\(apiHost)/v3/events") else { - completionHandler(.odpEventFailed("Invalid url")) - return - } - - let combinedData: [String: Any] = [ - "type": kinds[0], - "action": kinds[1], - //"data_source": "fullstack:swift-sdk", - "identifiers": identifiers, - "data": data - ] - - guard let body = try? JSONSerialization.data(withJSONObject: combinedData) else { - completionHandler(.odpEventFailed("Invalid JSON")) - 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") - - print("[ODP] request body: \(combinedData)") - - 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 - if let error = error { - completionHandler(.odpEventFailed(error.localizedDescription)) - return - } - - if let response = response as? HTTPURLResponse { - if response.statusCode >= 400 { - var message = "UNKNOWN" - if let data = data, let msg = String(bytes: data, encoding: .utf8) { - message = msg - } - completionHandler(.odpEventFailed(message)) - return - } - } - - completionHandler(nil) - } - - task.resume() - } - -} - // MARK: - Utils struct ODPAudience: Decodable { diff --git a/Sources/ODP/ZaiusRestApiManager.swift b/Sources/ODP/ZaiusRestApiManager.swift new file mode 100644 index 00000000..076ead11 --- /dev/null +++ b/Sources/ODP/ZaiusRestApiManager.swift @@ -0,0 +1,92 @@ +// +// 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 + +// MARK: - REST API + +class ZaiusRestApiManager { + + func sendODPEvent(apiKey: String, + apiHost: String, + identifiers: [String: Any], + kind: String, + data: [String: Any] = [:], + completionHandler: @escaping (OptimizelyError?) -> Void) { + let kinds = kind.split(separator: ":") + guard kinds.count == 2 else { + completionHandler(.odpEventFailed("Invalid format for kind")) + return + } + + guard let url = URL(string: "\(apiHost)/v3/events") else { + completionHandler(.odpEventFailed("Invalid url")) + return + } + + let combinedData: [String: Any] = [ + "type": kinds[0], + "action": kinds[1], + //"data_source": "fullstack:swift-sdk", + "identifiers": identifiers, + "data": data + ] + + guard let body = try? JSONSerialization.data(withJSONObject: combinedData) else { + completionHandler(.odpEventFailed("Invalid JSON")) + 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") + + print("[ODP] request body: \(combinedData)") + + 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 + if let error = error { + completionHandler(.odpEventFailed(error.localizedDescription)) + return + } + + if let response = response as? HTTPURLResponse { + if response.statusCode >= 400 { + var message = "UNKNOWN" + if let data = data, let msg = String(bytes: data, encoding: .utf8) { + message = msg + } + completionHandler(.odpEventFailed(message)) + return + } + } + + completionHandler(nil) + } + + task.resume() + } + + func getSession() -> URLSession { + return URLSession(configuration: .ephemeral) + } + +} diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 17d8205b..87e81f1b 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -53,23 +53,7 @@ open class OptimizelyClient: NSObject { var decisionService: OPTDecisionService! public var notificationCenter: OPTNotificationCenter? - - var odpConfig: OptimizelyODPConfig - private var atomicODPManager = AtomicProperty() - var odpManager: ODPManager { - get { - // instantiated on the first call (not instantiated when it's not used) - guard let handler = atomicODPManager.property else { - let defaultHandler = ODPManager(odpConfig: odpConfig) - atomicODPManager.property = defaultHandler - return defaultHandler - } - return handler - } - set { - atomicODPManager.property = newValue - } - } + var odpManager: ODPManager // MARK: - Public interfaces @@ -95,7 +79,7 @@ open class OptimizelyClient: NSObject { self.sdkKey = sdkKey self.defaultDecideOptions = defaultDecideOptions ?? [] - self.odpConfig = odpConfig ?? OptimizelyODPConfig() + self.odpManager = ODPManager(odpConfig: odpConfig ?? OptimizelyODPConfig()) super.init() @@ -210,6 +194,8 @@ open class OptimizelyClient: NSObject { do { self.config = try ProjectConfig(datafile: datafile) + odpManager.updateODPConfig(apiKey: config?.publicKeyForODP, apiHost: config?.hostForODP) + datafileHandler?.startUpdates(sdkKey: self.sdkKey) { data in // new datafile came in self.updateConfigFromBackgroundFetch(data: data) @@ -945,9 +931,7 @@ extension OptimizelyClient { completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { let segmentsToCheck = options.contains(.useSubset) ? config?.allSegments : nil - odpManager.fetchQualifiedSegments(apiKey: config?.publicKeyForODP, - apiHost: config?.hostForODP, - userKey: userKey, + odpManager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: segmentsToCheck, options: options, From a747d2c1e18a92afe392f6de8852328898256cc1 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 24 Jun 2022 10:59:49 -0700 Subject: [PATCH 42/84] create user context with vuid --- Sources/Data Model/ProjectConfig.swift | 23 ++++++---- Sources/ODP/ODPEventManager.swift | 6 +-- Sources/ODP/ODPManager.swift | 10 ++++- Sources/ODP/VUIDManager.swift | 13 +++++- Sources/ODP/ZaiusGraphQLApiManager.swift | 5 --- .../OptimizelyUserContext.swift | 42 +++++++++++-------- Sources/Optimizely/OptimizelyClient.swift | 10 +++-- Sources/Utils/Constants.swift | 6 ++- .../ZaiusApiManagerTests.swift | 3 -- 9 files changed, 73 insertions(+), 45 deletions(-) diff --git a/Sources/Data Model/ProjectConfig.swift b/Sources/Data Model/ProjectConfig.swift index 0c0a1060..00d7f7d9 100644 --- a/Sources/Data Model/ProjectConfig.swift +++ b/Sources/Data Model/ProjectConfig.swift @@ -193,14 +193,6 @@ extension ProjectConfig { // old versions (< 4) of datafiles not supported return ["4"].contains(version) } - - var publicKeyForODP: String? { - return project.integrations?.filter { $0.key == "odp" }.first?.publicKey - } - - var hostForODP: String? { - return project.integrations?.filter { $0.key == "odp" }.first?.host - } } // MARK: - Project Access @@ -214,6 +206,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. */ @@ -375,5 +381,4 @@ extension ProjectConfig { return nil } - } diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index 34dc12aa..68176c17 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -41,7 +41,7 @@ class ODPEventManager { func registerVUID(vuid: String) { let identifiers = [ - "vuid": vuid + Constants.ODP.keyForVuid: vuid ] queue.async { @@ -52,8 +52,8 @@ class ODPEventManager { func identifyUser(vuid: String, userId: String) { let identifiers = [ - "vuid": vuid, - "fs_user_id": userId + Constants.ODP.keyForVuid: vuid, + Constants.ODP.keyForUserId: userId ] queue.async { diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index 4237d961..242c09e8 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -35,11 +35,13 @@ class ODPManager { } } - func fetchQualifiedSegments(userKey: String, - userValue: String, + func fetchQualifiedSegments(userId: String, segmentsToCheck: [String]? = nil, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + let userKey = vuidManager.isVuid(visitorId: userId) ? Constants.ODP.keyForVuid : Constants.ODP.keyForUserId + let userValue = userId + segmentManager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: segmentsToCheck, @@ -69,4 +71,8 @@ class ODPManager { eventManager.flush() } + var vuid: String { + return vuidManager.vuid + } + } diff --git a/Sources/ODP/VUIDManager.swift b/Sources/ODP/VUIDManager.swift index 0b959621..8cb6c453 100644 --- a/Sources/ODP/VUIDManager.swift +++ b/Sources/ODP/VUIDManager.swift @@ -65,7 +65,18 @@ class VUIDManager { self.vuidMap.save() } } +} + +// MAKR: - VUID format + +extension VUIDManager { + static func makeVuid() -> String { + return "VUID_" + UUID().uuidString.replacingOccurrences(of: "-", with: "") + } + func isVuid(visitorId: String) -> Bool { + return visitorId.starts(with: "VUID") + } } // MARK: - VUIDMap @@ -114,7 +125,7 @@ struct VUIDMap { guard let vuids = UserDefaults.standard.dictionary(forKey: keyForVuidMap), let oldVuid = vuids[keyForVuid] as? String else { - self.vuid = "VUID-" + UUID().uuidString.replacingOccurrences(of: "-", with: "") + self.vuid = VUIDManager.makeVuid() self.registered = false self.users = [] self.usersSet = Set(users) diff --git a/Sources/ODP/ZaiusGraphQLApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift index 4e409c3e..e0009905 100644 --- a/Sources/ODP/ZaiusGraphQLApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -86,11 +86,6 @@ class ZaiusGraphQLApiManager { userValue: String, segmentsToCheck: [String]?, completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - if userKey != "vuid" { - completionHandler(nil, .fetchSegmentsFailed("userKeys other than 'vuid' not supported yet")) - return - } - let subsetFilter = makeSubsetFilter(segments: segmentsToCheck) let body = [ diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 08682a43..55f22778 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -57,7 +57,7 @@ public class OptimizelyUserContext { return userContext } - + let logger = OPTLoggerFactory.getLogger() /// OptimizelyUserContext init @@ -66,19 +66,35 @@ 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 ?? [:]) + self.optimizely?.registerUserToODP(userId: userId) + } + + /// OptimizelyUserContext init for vuid-based decision + /// + /// When a userId is not provided, a user context will be created with the device vuid as a default user id. + /// + /// - Parameters: + /// - optimizely: An instance of OptimizelyClient to be used for decisions. + /// - attributes: A map of attribute names to current user attribute values. + public convenience init(optimizely: OptimizelyClient, + attributes: [String: Any?]? = nil) { + self.init(optimizely: optimizely, userId: optimizely.vuid, attributes: attributes ?? [:]) + } + + init(optimizely: OptimizelyClient, + userId: String, + attributes: [String: Any?]) { self.optimizely = optimizely self.userId = userId let lock = DispatchQueue(label: "user-context") - self.atomicAttributes = AtomicProperty(property: attributes ?? [:], lock: lock) + self.atomicAttributes = AtomicProperty(property: attributes, lock: lock) self.atomicForcedDecisions = AtomicProperty(property: nil, lock: lock) self.atomicQualifiedSegments = AtomicProperty(property: nil, lock: lock) - - // USERID only (not VUID) - self.optimizely?.registerUserToODP(userId: userId) } /// Sets an attribute for a given key. @@ -185,16 +201,8 @@ extension OptimizelyUserContext { completionHandler(nil, .sdkNotReady) return } - - - // VUID or USERID ??? - let userKey = userKey ?? Constants.Attributes.reservedUserIdKey - let userValue = userValue ?? userId - - optimizely.fetchQualifiedSegments(userKey: userKey, - userValue: userValue, - options: options) { segments, err in + 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) diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 87e81f1b..cc55f11f 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -921,18 +921,20 @@ extension OptimizelyClient { extension OptimizelyClient { + var vuid: String { + return odpManager.vuid + } + func registerUserToODP(userId: String) { odpManager.identifyUser(userId: userId) } - func fetchQualifiedSegments(userKey: String, - userValue: String, + func fetchQualifiedSegments(userId: String, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { let segmentsToCheck = options.contains(.useSubset) ? config?.allSegments : nil - odpManager.fetchQualifiedSegments(userKey: userKey, - userValue: userValue, + odpManager.fetchQualifiedSegments(userId: userId, segmentsToCheck: segmentsToCheck, options: options, completionHandler: completionHandler) diff --git a/Sources/Utils/Constants.swift b/Sources/Utils/Constants.swift index 2ccd6435..3e899fad 100644 --- a/Sources/Utils/Constants.swift +++ b/Sources/Utils/Constants.swift @@ -21,7 +21,11 @@ struct Constants { static let reservedBucketIdAttribute = "$opt_bucketing_id" static let reservedBotFilteringAttribute = "$opt_bot_filtering" static let reservedUserAgent = "$opt_user_agent" - static let reservedUserIdKey = "$opt_user_id" + } + + struct ODP { + static let keyForVuid = "vuid" + static let keyForUserId = "fs_user_id" } enum EvaluationLogType: String { diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift index e957cfd6..106669b9 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift @@ -17,9 +17,6 @@ import XCTest class ZaiusApiManagerTests: XCTestCase { - - // TODO: currently "vuid" only supported - //var userKey = "test-user-key" let userKey = "vuid" let userValue = "test-user-value" From 980cbdfad5daef7494e6153c162ef013dbe15020 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 24 Jun 2022 18:07:38 -0700 Subject: [PATCH 43/84] fix ODPEventsManager for reliable and batch dispatch --- Sources/ODP/ODPEventManager.swift | 141 +++++++++++++++++--------- Sources/ODP/ODPManager.swift | 10 +- Sources/ODP/VUIDManager.swift | 2 - Sources/ODP/ZaiusRestApiManager.swift | 28 +---- 4 files changed, 105 insertions(+), 76 deletions(-) diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index 68176c17..3143f410 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -16,83 +16,128 @@ import Foundation -struct ODPEvent { - let kind: String - let identifiers: [String: Any] - let data: [String: Any] + +struct ODPEvent: Codable { + let type: String + let action: String + // TODO: change to [String: Any] to support arbitary value types + let identifiers: [String: String] + let data: [String: String] + + //let data_source = "fullstack:swift-sdk" } class ODPEventManager { let odpConfig: OptimizelyODPConfig - var events: [ODPEvent] - let queue: DispatchQueue let zaiusMgr: ZaiusRestApiManager + let maxQueueSize = 100 + let maxFailureCount = 3 + let queueLock: DispatchQueue + let eventQueue: DataStoreQueueStackImpl + let logger = OPTLoggerFactory.getLogger() init(odpConfig: OptimizelyODPConfig) { self.odpConfig = odpConfig - self.events = [] - self.queue = DispatchQueue(label: "event") self.zaiusMgr = ZaiusRestApiManager() + + self.queueLock = DispatchQueue(label: "event") + self.eventQueue = DataStoreQueueStackImpl(queueStackName: "odp", + dataStore: DataStoreFile<[Data]>(storeName: "OPT-ODPEvent")) } // MARK: - ODP API func registerVUID(vuid: String) { - let identifiers = [ - Constants.ODP.keyForVuid: vuid - ] - - queue.async { - self.events.append(ODPEvent(kind: "experimentation:client_initialized", identifiers: identifiers, data: [:])) - self.flushEvents(self.events) - } + let event = ODPEvent(type: "experimentation", + action: "client_initialized", + identifiers: [ + Constants.ODP.keyForVuid: vuid + ], + data: [:]) + dispatchEvent(event) } func identifyUser(vuid: String, userId: String) { - let identifiers = [ - Constants.ODP.keyForVuid: vuid, - Constants.ODP.keyForUserId: userId - ] - - queue.async { - self.events.append(ODPEvent(kind: "experimentation:identified", identifiers: identifiers, data: [:])) - self.flushEvents(self.events) - } + let event = ODPEvent(type: "experimentation", + action: "identified", + identifiers: [ + Constants.ODP.keyForVuid: vuid, + Constants.ODP.keyForUserId: userId + ], + data: [:]) + dispatchEvent(event) } - // MARK: - Events - - func flush() { - queue.async { - self.flushEvents(self.events) + func dispatchEvent(_ event: ODPEvent) { + guard eventQueue.count < maxQueueSize else { + let error = OptimizelyError.eventDispatchFailed("ODP EventQueue is full") + self.logger.e(error) + return } + + eventQueue.save(item: event) + flushEvents() } - private func flushEvents(_ events: [ODPEvent]) { + // MARK: - Events + + func flushEvents() { guard let odpApiKey = odpConfig.apiKey else { - logger.d("ODP event cannot be dispatched since apiKey not defined") + logger.d("ODP: event cannot be dispatched since apiKey not defined") return } - - for event in events { - sendODPEvent(event, apiKey: odpApiKey, apiHost: odpConfig.apiHost) - } - } - - func sendODPEvent(_ event: ODPEvent, apiKey: String, apiHost: String) { - zaiusMgr.sendODPEvent(apiKey: apiKey, - apiHost: apiHost, - identifiers: event.identifiers, - kind: event.kind, - data: event.data) { error in - if error != nil { - self.logger.w("ODP event dispatch failed: \(error!)") + + 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") + } + } + + // notify group used to ensure that the sendEvent is synchronous. + // used in flushEvents + let notify = DispatchGroup() + + let maxBatchEvents = 10 + var failureCount = 0 + + while let events: [ODPEvent] = self.eventQueue.getFirstItems(count: maxBatchEvents) { + let numEvents = events.count + + // we've exhuasted our failure count. Give up and try the next time a event + // is queued or someone calls flush (changed to >= so that retried exactly "maxFailureCount" times). + if failureCount >= self.maxFailureCount { + self.logger.e("ODP: Failed to send event with max retried") + break + } + + // make the send event synchronous. enter our notify + notify.enter() + + self.zaiusMgr.sendODPEvents(apiKey: odpApiKey, + apiHost: self.odpConfig.apiHost, + events: events) { error in + if error != nil { + self.logger.e(error!.reason) + failureCount += 1 + } else { + removeStoredEvents(num: numEvents) + failureCount = 0 + } + + // our send is done. + notify.leave() + } + + // wait for send + notify.wait() } } } } - - diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index 242c09e8..80f44e36 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -18,15 +18,19 @@ import Foundation class ODPManager { let odpConfig: OptimizelyODPConfig + let vuidManager: VUIDManager let segmentManager: ODPSegmentManager let eventManager: ODPEventManager let logger = OPTLoggerFactory.getLogger() - init(odpConfig: OptimizelyODPConfig, vuidManager: VUIDManager? = nil, segmentManager: ODPSegmentManager? = nil, eventManager: ODPEventManager? = nil) { + init(odpConfig: OptimizelyODPConfig, + vuidManager: VUIDManager? = nil, + segmentManager: ODPSegmentManager? = nil, + eventManager: ODPEventManager? = nil) { self.odpConfig = odpConfig - self.vuidManager = vuidManager ?? VUIDManager.shared + self.vuidManager = vuidManager ?? VUIDManager() self.segmentManager = segmentManager ?? ODPSegmentManager(odpConfig: odpConfig) self.eventManager = eventManager ?? ODPEventManager(odpConfig: odpConfig) @@ -68,7 +72,7 @@ class ODPManager { odpConfig.apiHost = apiHost // flush all ODP events waiting for apiKey - eventManager.flush() + eventManager.flushEvents() } var vuid: String { diff --git a/Sources/ODP/VUIDManager.swift b/Sources/ODP/VUIDManager.swift index 8cb6c453..0eb3ccf7 100644 --- a/Sources/ODP/VUIDManager.swift +++ b/Sources/ODP/VUIDManager.swift @@ -17,8 +17,6 @@ import Foundation class VUIDManager { - static let shared = VUIDManager() - var vuidMap: VUIDMap let queue: DispatchQueue let logger = OPTLoggerFactory.getLogger() diff --git a/Sources/ODP/ZaiusRestApiManager.swift b/Sources/ODP/ZaiusRestApiManager.swift index 076ead11..45703c3c 100644 --- a/Sources/ODP/ZaiusRestApiManager.swift +++ b/Sources/ODP/ZaiusRestApiManager.swift @@ -20,32 +20,16 @@ import Foundation class ZaiusRestApiManager { - func sendODPEvent(apiKey: String, - apiHost: String, - identifiers: [String: Any], - kind: String, - data: [String: Any] = [:], - completionHandler: @escaping (OptimizelyError?) -> Void) { - let kinds = kind.split(separator: ":") - guard kinds.count == 2 else { - completionHandler(.odpEventFailed("Invalid format for kind")) - return - } - + func sendODPEvents(apiKey: String, + apiHost: String, + events: [ODPEvent], + completionHandler: @escaping (OptimizelyError?) -> Void) { guard let url = URL(string: "\(apiHost)/v3/events") else { completionHandler(.odpEventFailed("Invalid url")) return } - let combinedData: [String: Any] = [ - "type": kinds[0], - "action": kinds[1], - //"data_source": "fullstack:swift-sdk", - "identifiers": identifiers, - "data": data - ] - - guard let body = try? JSONSerialization.data(withJSONObject: combinedData) else { + guard let body = try? JSONSerialization.data(withJSONObject: events) else { completionHandler(.odpEventFailed("Invalid JSON")) return } @@ -55,8 +39,6 @@ class ZaiusRestApiManager { urlRequest.httpBody = body urlRequest.addValue(apiKey, forHTTPHeaderField: "x-api-key") urlRequest.addValue("application/json", forHTTPHeaderField: "content-type") - - print("[ODP] request body: \(combinedData)") let session = self.getSession() // without this the URLSession will leak, see docs on URLSession and https://stackoverflow.com/questions/67318867 From 3601932ee939234b408e81872484f46ac62a6e62 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 27 Jun 2022 14:27:20 -0700 Subject: [PATCH 44/84] vuid manager singleton --- Sources/ODP/ODPEventManager.swift | 23 +++++++++++++++-------- Sources/ODP/ODPManager.swift | 18 +++++++++++++----- Sources/ODP/VUIDManager.swift | 4 +++- Sources/ODP/ZaiusRestApiManager.swift | 4 ++++ Sources/Optimizely/OptimizelyClient.swift | 3 ++- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index 3143f410..5212fa77 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -16,7 +16,6 @@ import Foundation - struct ODPEvent: Codable { let type: String let action: String @@ -24,7 +23,10 @@ struct ODPEvent: Codable { let identifiers: [String: String] let data: [String: String] - //let data_source = "fullstack:swift-sdk" + // TODO: need data source later + // let data_source = "fullstack:swift-sdk" + + var completion: (() -> Void)? } class ODPEventManager { @@ -38,35 +40,40 @@ class ODPEventManager { let logger = OPTLoggerFactory.getLogger() - init(odpConfig: OptimizelyODPConfig) { + init(sdkKey: String, odpConfig: OptimizelyODPConfig) { self.odpConfig = odpConfig self.zaiusMgr = ZaiusRestApiManager() self.queueLock = DispatchQueue(label: "event") + + // a separate event queue for each sdkKey (which may have own ODP public key) + let storeName = "OPDEvent-\(sdkKey)" self.eventQueue = DataStoreQueueStackImpl(queueStackName: "odp", - dataStore: DataStoreFile<[Data]>(storeName: "OPT-ODPEvent")) + dataStore: DataStoreFile<[Data]>(storeName: storeName)) } // MARK: - ODP API - func registerVUID(vuid: String) { + func registerVUID(vuid: String, completion: @escaping () -> Void) { let event = ODPEvent(type: "experimentation", action: "client_initialized", identifiers: [ Constants.ODP.keyForVuid: vuid ], - data: [:]) + data: [:], + completion: completion) dispatchEvent(event) } - func identifyUser(vuid: String, userId: String) { + func identifyUser(vuid: String, userId: String, completion: @escaping () -> Void) { let event = ODPEvent(type: "experimentation", action: "identified", identifiers: [ Constants.ODP.keyForVuid: vuid, Constants.ODP.keyForUserId: userId ], - data: [:]) + data: [:], + completion: completion) dispatchEvent(event) } diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index 80f44e36..523e5160 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -25,17 +25,22 @@ class ODPManager { let logger = OPTLoggerFactory.getLogger() - init(odpConfig: OptimizelyODPConfig, + init(sdkKey: String, + odpConfig: OptimizelyODPConfig, vuidManager: VUIDManager? = nil, segmentManager: ODPSegmentManager? = nil, eventManager: ODPEventManager? = nil) { self.odpConfig = odpConfig - self.vuidManager = vuidManager ?? VUIDManager() + self.vuidManager = vuidManager ?? VUIDManager.shared self.segmentManager = segmentManager ?? ODPSegmentManager(odpConfig: odpConfig) - self.eventManager = eventManager ?? ODPEventManager(odpConfig: odpConfig) + self.eventManager = eventManager ?? ODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) if !self.vuidManager.isVUIDRegistered { - self.eventManager.registerVUID(vuid: self.vuidManager.vuid) + let vuid = self.vuidManager.vuid + self.eventManager.registerVUID(vuid: vuid) { + self.logger.d("ODP: vuid has been registered (\(vuid)).") + self.vuidManager.setVUIDRegistered() + } } } @@ -59,7 +64,10 @@ class ODPManager { return } - eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) + eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) { + self.logger.d("ODP: userId (\(userId)) has been bound to (\(self.vuidManager.vuid)).") + self.vuidManager.addRegisteredUser(userId: userId) + } } func updateODPConfig(apiKey: String?, apiHost: String?) { diff --git a/Sources/ODP/VUIDManager.swift b/Sources/ODP/VUIDManager.swift index 0eb3ccf7..e355b6a4 100644 --- a/Sources/ODP/VUIDManager.swift +++ b/Sources/ODP/VUIDManager.swift @@ -21,6 +21,9 @@ class VUIDManager { let queue: DispatchQueue let logger = OPTLoggerFactory.getLogger() + // a single instance (vuid as a device id) should be shared for all SDK instances + static let shared = VUIDManager() + init() { self.queue = DispatchQueue(label: "vuid") self.vuidMap = VUIDMap() @@ -144,4 +147,3 @@ struct VUIDMap { UserDefaults.standard.synchronize() } } - diff --git a/Sources/ODP/ZaiusRestApiManager.swift b/Sources/ODP/ZaiusRestApiManager.swift index 45703c3c..423999b8 100644 --- a/Sources/ODP/ZaiusRestApiManager.swift +++ b/Sources/ODP/ZaiusRestApiManager.swift @@ -61,6 +61,10 @@ class ZaiusRestApiManager { } } + events.forEach { event in + event.completion?() + } + completionHandler(nil) } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index cc55f11f..1b5a89cb 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -79,7 +79,8 @@ open class OptimizelyClient: NSObject { self.sdkKey = sdkKey self.defaultDecideOptions = defaultDecideOptions ?? [] - self.odpManager = ODPManager(odpConfig: odpConfig ?? OptimizelyODPConfig()) + self.odpManager = ODPManager(sdkKey: sdkKey, + odpConfig: odpConfig ?? OptimizelyODPConfig()) super.init() From 1770adadf2c8d58097b7a147c4d3aac9ee84e036 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 29 Jun 2022 14:02:30 -0700 Subject: [PATCH 45/84] odp events ready --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 34 +++++ Sources/ODP/ODPEvent.swift | 68 ++++++++++ Sources/ODP/ODPEventManager.swift | 62 ++++----- Sources/ODP/ODPManager.swift | 22 +-- Sources/ODP/OptimizelySegmentOption.swift | 2 - Sources/ODP/VUIDManager.swift | 127 ++++-------------- Sources/ODP/ZaiusGraphQLApiManager.swift | 4 +- Sources/ODP/ZaiusRestApiManager.swift | 6 +- .../OptimizelyClient+Decide.swift | 5 +- .../OptimizelyUserContext.swift | 13 +- Sources/Optimizely/OptimizelyClient.swift | 24 +++- Sources/Utils/Constants.swift | 1 + 12 files changed, 202 insertions(+), 166 deletions(-) create mode 100644 Sources/ODP/ODPEvent.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index f4faa2ff..1ca62fd2 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1825,6 +1825,22 @@ 848617F72863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; 848617F82863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; 848617F92863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.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 */; }; 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 */; }; @@ -2335,6 +2351,7 @@ 848617C72863DC2700B7F41B /* ODPSegmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPSegmentManager.swift; sourceTree = ""; }; 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusGraphQLApiManager.swift; sourceTree = ""; }; 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusRestApiManager.swift; sourceTree = ""; }; + 848617FA286CF33700B7F41B /* ODPEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPEvent.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 /* VUIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VUIDManager.swift; sourceTree = ""; }; @@ -2559,6 +2576,7 @@ 84E2E9412852A378001114AB /* VUIDManager.swift */, 848617C72863DC2700B7F41B /* ODPSegmentManager.swift */, 84E2E9712855875E001114AB /* ODPEventManager.swift */, + 848617FA286CF33700B7F41B /* ODPEvent.swift */, 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */, 6E6522FF278E688B00954EA1 /* LRUCache.swift */, @@ -4011,6 +4029,7 @@ 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 */, @@ -4187,6 +4206,7 @@ 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 */, @@ -4251,6 +4271,7 @@ 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 */, @@ -4309,6 +4330,7 @@ 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 */, @@ -4419,6 +4441,7 @@ 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 */, @@ -4599,6 +4622,7 @@ 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; @@ -4612,6 +4636,7 @@ 848617F72863E21200B7F41B /* ZaiusRestApiManager.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_Segments_Decide.swift in Sources */, @@ -4764,6 +4789,7 @@ 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 /* VUIDManager.swift in Sources */, @@ -4888,6 +4914,7 @@ 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 */, 8464087528130D3200CCF97D /* Integration.swift in Sources */, @@ -5022,6 +5049,7 @@ 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 /* VUIDManager.swift in Sources */, @@ -5163,6 +5191,7 @@ 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 */, @@ -5262,6 +5291,7 @@ 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 */, @@ -5368,6 +5398,7 @@ 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 */, @@ -5426,6 +5457,7 @@ 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 */, @@ -5590,6 +5622,7 @@ 75C71A4025E454460084187E /* Notifications.swift in Sources */, 75C71A4125E454460084187E /* MurmurHash3.swift in Sources */, 848617ED2863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, + 848617FE286CF33700B7F41B /* ODPEvent.swift in Sources */, 75C71A4225E454460084187E /* HandlerRegistryService.swift in Sources */, 75C71A4325E454460084187E /* LogMessage.swift in Sources */, 75C71A4425E454460084187E /* AtomicProperty.swift in Sources */, @@ -5651,6 +5684,7 @@ 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 */, diff --git a/Sources/ODP/ODPEvent.swift b/Sources/ODP/ODPEvent.swift new file mode 100644 index 00000000..a5531452 --- /dev/null +++ b/Sources/ODP/ODPEvent.swift @@ -0,0 +1,68 @@ +// +// 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 + ] + } +} diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index 5212fa77..aa498255 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -16,19 +16,6 @@ import Foundation -struct ODPEvent: Codable { - let type: String - let action: String - // TODO: change to [String: Any] to support arbitary value types - let identifiers: [String: String] - let data: [String: String] - - // TODO: need data source later - // let data_source = "fullstack:swift-sdk" - - var completion: (() -> Void)? -} - class ODPEventManager { let odpConfig: OptimizelyODPConfig let zaiusMgr: ZaiusRestApiManager @@ -52,45 +39,60 @@ class ODPEventManager { dataStore: DataStoreFile<[Data]>(storeName: storeName)) } - // MARK: - ODP API + // MARK: - events - func registerVUID(vuid: String, completion: @escaping () -> Void) { - let event = ODPEvent(type: "experimentation", + func registerVUID(vuid: String) { + let event = ODPEvent(type: Constants.ODP.eventType, action: "client_initialized", identifiers: [ Constants.ODP.keyForVuid: vuid ], - data: [:], - completion: completion) - dispatchEvent(event) + data: addCommonEventData()) + dispatch(event) } - func identifyUser(vuid: String, userId: String, completion: @escaping () -> Void) { - let event = ODPEvent(type: "experimentation", + func identifyUser(vuid: String, userId: String) { + let event = ODPEvent(type: Constants.ODP.eventType, action: "identified", identifiers: [ Constants.ODP.keyForVuid: vuid, Constants.ODP.keyForUserId: userId ], - data: [:], - completion: completion) - dispatchEvent(event) + data: addCommonEventData()) + dispatch(event) + } + + 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 dispatchEvent(_ event: ODPEvent) { + func dispatch(_ event: ODPEvent) { guard eventQueue.count < maxQueueSize else { let error = OptimizelyError.eventDispatchFailed("ODP EventQueue is full") self.logger.e(error) return } - + eventQueue.save(item: event) - flushEvents() + flush() } - // MARK: - Events - - func flushEvents() { + func addCommonEventData(_ customData: [String: Any] = [:]) -> [String: Any] { + let commonData = [ + "source": "swift-sdk" + // others? + ] + + var data = customData + data.merge(commonData) { (current, _) in current } // keep custom data if conflicts + return data + } + + func flush() { guard let odpApiKey = odpConfig.apiKey else { logger.d("ODP: event cannot be dispatched since apiKey not defined") return diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index 523e5160..15c2ea4b 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -35,13 +35,7 @@ class ODPManager { self.segmentManager = segmentManager ?? ODPSegmentManager(odpConfig: odpConfig) self.eventManager = eventManager ?? ODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) - if !self.vuidManager.isVUIDRegistered { - let vuid = self.vuidManager.vuid - self.eventManager.registerVUID(vuid: vuid) { - self.logger.d("ODP: vuid has been registered (\(vuid)).") - self.vuidManager.setVUIDRegistered() - } - } + self.eventManager.registerVUID(vuid: self.vuidManager.vuid) } func fetchQualifiedSegments(userId: String, @@ -59,15 +53,11 @@ class ODPManager { } func identifyUser(userId: String) { - if vuidManager.isUserRegistered(userId: userId) { - logger.d("ODP: user (\(userId)) is registered already.") - return - } - - eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) { - self.logger.d("ODP: userId (\(userId)) has been bound to (\(self.vuidManager.vuid)).") - self.vuidManager.addRegisteredUser(userId: userId) - } + eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) + } + + func sendEvent(type: String, action: String, identifiers: [String: Any], data: [String: Any]) { + eventManager.sendEvent(type: type, action: action, identifiers: identifiers, data: data) } func updateODPConfig(apiKey: String?, apiHost: String?) { diff --git a/Sources/ODP/OptimizelySegmentOption.swift b/Sources/ODP/OptimizelySegmentOption.swift index 12e12712..dd186ca1 100644 --- a/Sources/ODP/OptimizelySegmentOption.swift +++ b/Sources/ODP/OptimizelySegmentOption.swift @@ -18,8 +18,6 @@ import Foundation /// Options controlling audience segments. @objc public enum OptimizelySegmentOption: Int { - // fetch subset segments only (used in the project). default is fetch-all-segments - case useSubset // ignore cache (save/lookup) case ignoreCache // reset cache diff --git a/Sources/ODP/VUIDManager.swift b/Sources/ODP/VUIDManager.swift index e355b6a4..27a50106 100644 --- a/Sources/ODP/VUIDManager.swift +++ b/Sources/ODP/VUIDManager.swift @@ -17,61 +17,17 @@ import Foundation class VUIDManager { - var vuidMap: VUIDMap - let queue: DispatchQueue + var vuid: String = "" let logger = OPTLoggerFactory.getLogger() - // a single instance (vuid as a device id) should be shared for all SDK instances + // a single vuid should be shared for all SDK instances static let shared = VUIDManager() init() { - self.queue = DispatchQueue(label: "vuid") - self.vuidMap = VUIDMap() - } - - var vuid: String { - var vuid = "" - queue.sync { - vuid = self.vuidMap.vuid - } - return vuid - } - - var isVUIDRegistered: Bool { - var registered = false - queue.sync { - registered = self.vuidMap.registered - } - return registered - } - - func setVUIDRegistered() { - queue.async { - self.vuidMap.registered = true - self.vuidMap.save() - } - } - - func isUserRegistered(userId: String) -> Bool { - var registered = false - queue.sync { - registered = self.vuidMap.usersSet.contains(userId) - } - return registered + self.vuid = load() } - func addRegisteredUser(userId: String) { - queue.async { - self.vuidMap.addUser(userId) - self.vuidMap.save() - } - } -} - -// MAKR: - VUID format - -extension VUIDManager { - static func makeVuid() -> String { + func makeVuid() -> String { return "VUID_" + UUID().uuidString.replacingOccurrences(of: "-", with: "") } @@ -80,70 +36,37 @@ extension VUIDManager { } } -// MARK: - VUIDMap +// MARK: - VUID Store -struct VUIDMap { - var vuid: String = "" - var registered: Bool = false - var users = [String]() - var usersSet = Set() - - let maxUsersRegistered = 10 - - init() { - self.load() - } - - mutating func addUser(_ userId: String) { - if self.usersSet.contains(userId) { - return - } - - if users.count > maxUsersRegistered { - users.removeFirst() - } - users.append(userId) - - usersSet = Set(users) - save() - } - - // MARK: - UserDefaults +extension VUIDManager { // UserDefaults format: (keep the most recent vuid info only) - // "optimizely-vuids": { - // "vuid": "vuid1", - // "registered": true, - // "users": ["userId1", "userId2"] + // "optimizely-odp": { + // "vuid": "vuid1" // } - - var keyForVuidMap = "optimizely-vuid-map" - var keyForVuid = "vuid" - var keyForRegistered = "registered" - var keyForUsers = "users" - mutating func load() { - guard let vuids = UserDefaults.standard.dictionary(forKey: keyForVuidMap), - let oldVuid = vuids[keyForVuid] as? String - else { - self.vuid = VUIDManager.makeVuid() - self.registered = false - self.users = [] - self.usersSet = Set(users) - save() - return + private var keyForVuidMap: String { + return "optimizely-odp" + } + private var keyForVuid: String { + "vuid" + } + + private func load() -> String { + if let vuids = UserDefaults.standard.dictionary(forKey: keyForVuidMap), + let oldVuid = vuids[keyForVuid] as? String { + return oldVuid } - self.vuid = oldVuid - self.registered = (vuids[keyForRegistered] as? Bool) ?? false - self.users = (vuids[keyForUsers] as? [String]) ?? [] - self.usersSet = Set(users) + let vuid = makeVuid() + save(vuid: vuid) + return vuid } - func save() { - let dict: [String: Any] = [keyForVuid: vuid, keyForRegistered: registered, keyForUsers: users] + private func save(vuid: String) { + let dict: [String: Any] = [keyForVuid: vuid] UserDefaults.standard.set(dict, forKey: keyForVuidMap) - print("saved vuidMap: \(dict)") UserDefaults.standard.synchronize() } + } diff --git a/Sources/ODP/ZaiusGraphQLApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift index e0009905..be860a73 100644 --- a/Sources/ODP/ZaiusGraphQLApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -31,8 +31,8 @@ import Foundation // fetch all segments curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql - // fetch info for "has_email" segment only - 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"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql + // fetch info 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") { diff --git a/Sources/ODP/ZaiusRestApiManager.swift b/Sources/ODP/ZaiusRestApiManager.swift index 423999b8..d154f5b1 100644 --- a/Sources/ODP/ZaiusRestApiManager.swift +++ b/Sources/ODP/ZaiusRestApiManager.swift @@ -29,7 +29,7 @@ class ZaiusRestApiManager { return } - guard let body = try? JSONSerialization.data(withJSONObject: events) else { + guard let body = try? JSONSerialization.data(withJSONObject: events.map{ $0.dict }) else { completionHandler(.odpEventFailed("Invalid JSON")) return } @@ -61,10 +61,6 @@ class ZaiusRestApiManager { } } - events.forEach { event in - event.completion?() - } - completionHandler(nil) } diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index 432ebaad..47d4191e 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -31,7 +31,10 @@ extension OptimizelyClient { return OptimizelyUserContext(optimizely: self, userId: userId, attributes: attributes) } - // VUID-based UserContext + /// 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 { let vuid = odpManager.vuidManager.vuid return OptimizelyUserContext(optimizely: self, userId: vuid, attributes: attributes) diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 55f22778..fa9cf7c9 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -32,6 +32,7 @@ public class OptimizelyUserContext { } 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 @@ -185,14 +186,11 @@ public class OptimizelyUserContext { extension OptimizelyUserContext { - /// Fetch all qualified segments for the given user identifier (**userKey** and **userValue**). + /// Fetch all qualified segments for the user context. /// - /// The **userId** of this context will be used by default when the user identifier is not provided. /// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time. /// /// - Parameters: - /// - userKey: The name of the user identifier (optional). - /// - userValue: The value of the user identifier (optional). /// - 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] = [], @@ -214,8 +212,11 @@ extension OptimizelyUserContext { completionHandler(segments, nil) } } - - // true if the user is qualified for the given segment name + + /// Check is the user 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 } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 1b5a89cb..2685de8f 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -922,6 +922,23 @@ extension OptimizelyClient { 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. + public func sendODPEvent(type: String? = nil, + action: String, + identifiers: [String: String] = [:], + data: [String: Any] = [:]) { + odpManager.sendEvent(type: type ?? Constants.ODP.eventType, + action: action, + identifiers: identifiers, + data: data) + } + var vuid: String { return odpManager.vuid } @@ -933,14 +950,17 @@ extension OptimizelyClient { func fetchQualifiedSegments(userId: String, options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - let segmentsToCheck = options.contains(.useSubset) ? config?.allSegments : nil + guard let segmentsToCheck = config?.allSegments else { + completionHandler(nil, .sdkNotReady) + return + } odpManager.fetchQualifiedSegments(userId: userId, segmentsToCheck: segmentsToCheck, options: options, completionHandler: completionHandler) } - + } // MARK: - For test support diff --git a/Sources/Utils/Constants.swift b/Sources/Utils/Constants.swift index 3e899fad..b9c65f05 100644 --- a/Sources/Utils/Constants.swift +++ b/Sources/Utils/Constants.swift @@ -26,6 +26,7 @@ struct Constants { struct ODP { static let keyForVuid = "vuid" static let keyForUserId = "fs_user_id" + static let eventType = "fullstack" } enum EvaluationLogType: String { From 564bbe9c4ca3d77d3f33770a50c64ed12b6b1178 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 Jul 2022 09:02:11 -0700 Subject: [PATCH 46/84] update odp event format --- .../xcschemes/DemoSwiftwatchOS.xcscheme | 25 +- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 48 ++- .../Events/BatchEventBuilder.swift | 6 +- Sources/ODP/LRUCache.swift | 20 +- Sources/ODP/ODPEventManager.swift | 39 ++- Sources/ODP/ODPManager.swift | 6 +- Sources/ODP/ODPSegmentManager.swift | 8 +- Sources/ODP/ZaiusGraphQLApiManager.swift | 75 +++-- Sources/ODP/ZaiusRestApiManager.swift | 14 +- .../OptimizelyUserContext.swift | 3 + Sources/Optimizely/OptimizelyError.swift | 6 +- Sources/Utils/Utils.swift | 17 +- .../OptimizelyClientTests_Decide.swift | 16 + .../ODPEventManagerTests.swift | 21 ++ .../ODPManagerTests.swift | 21 ++ ...wift => ODPSegmentManagerTests copy.swift} | 42 ++- ...sts.swift => ODPSegmentManagerTests.swift} | 69 +++-- .../ODPVUIDManagerTests.swift | 21 ++ .../ODPZaiusGraphQLApiManagerTests.swift | 292 ++++++++++++++++++ .../ODPZaiusRestApiManagerTests.swift | 269 ++++++++++++++++ .../OptimizelyUserContextTests_Segments.swift | 57 ++-- 21 files changed, 918 insertions(+), 157 deletions(-) create mode 100644 Tests/OptimizelyTests-Common/ODPEventManagerTests.swift create mode 100644 Tests/OptimizelyTests-Common/ODPManagerTests.swift rename Tests/OptimizelyTests-Common/{ZaiusApiManagerTests.swift => ODPSegmentManagerTests copy.swift} (83%) rename Tests/OptimizelyTests-Common/{AudienceSegmentsHandlerTests.swift => ODPSegmentManagerTests.swift} (60%) create mode 100644 Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift create mode 100644 Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift create mode 100644 Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift diff --git a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme index 1ebde8af..7aa2b41d 100644 --- a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme +++ b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme @@ -54,8 +54,10 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - + + + + + diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 1ca62fd2..f74a3c4e 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1755,10 +1755,6 @@ 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 */; }; - 8428D3D5280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; - 8428D3D7280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */; }; - 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */; }; - 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.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 */; }; @@ -1841,6 +1837,18 @@ 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 /* ODPZaiusGraphQLApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861819286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift */; }; + 8486181C286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861819286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift */; }; + 8486181D286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift */; }; + 8486181E286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.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 */; }; @@ -2344,14 +2352,18 @@ 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 = ""; }; - 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiusApiManagerTests.swift; sourceTree = ""; }; - 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceSegmentsHandlerTests.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 = ""; }; 848617C72863DC2700B7F41B /* ODPSegmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPSegmentManager.swift; sourceTree = ""; }; 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusGraphQLApiManager.swift; sourceTree = ""; }; 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusRestApiManager.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 /* ODPZaiusGraphQLApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPZaiusGraphQLApiManagerTests.swift; sourceTree = ""; }; + 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPZaiusRestApiManagerTests.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 /* VUIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VUIDManager.swift; sourceTree = ""; }; @@ -2853,7 +2865,6 @@ isa = PBXGroup; children = ( 84958C5D280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift */, - 84632F1028049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift */, 6E75198522C5211100B2B157 /* BatchEventBuilderTest.swift */, 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */, 6E75198722C5211100B2B157 /* BatchEventBuilderTests_Events.swift */, @@ -2880,6 +2891,12 @@ 6E75197F22C5211100B2B157 /* MurmurTests.swift */, 6E0207A7272A11CF008C3711 /* NetworkReachabilityTests.swift */, 6E75198B22C5211100B2B157 /* NotificationCenterTests.swift */, + 84861810286D0B8900B7F41B /* ODPEventManagerTests.swift */, + 8486180E286D0B8900B7F41B /* ODPManagerTests.swift */, + 8486180D286D0B8900B7F41B /* ODPSegmentManagerTests.swift */, + 8486180F286D0B8900B7F41B /* ODPVUIDManagerTests.swift */, + 84861819286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift */, + 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift */, 6E27EC9A266EF11000B4A6D4 /* OptimizelyDecisionTests.swift */, 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */, 6EC6DD6824AE94820017D296 /* OptimizelyUserContextTests.swift */, @@ -2889,7 +2906,6 @@ 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */, 6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */, 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */, - 8428D3D4280753F900D0FB0C /* ZaiusApiManagerTests.swift */, 6E86CEB224FF20DE005DAFED /* OptimizelyUserContextTests_Objc.m */, ); path = "OptimizelyTests-Common"; @@ -4645,6 +4661,7 @@ 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 */, @@ -4672,6 +4689,7 @@ 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 */, @@ -4691,18 +4709,18 @@ 6E7517D122C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6EA2CC2D2345618E001E7531 /* OptimizelyConfig.swift in Sources */, C78CAFAE24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, - 84632F1228049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */, 6E7517AB22C520D400B2B157 /* Array+Extension.swift in Sources */, + 8486181E286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift in Sources */, 6E75186122C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E75172722C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E7518FD22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7518E522C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E623F0D253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E424BE9263228E90081004A /* AtomicArray.swift in Sources */, - 8428D3D7280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */, 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 */, @@ -4723,6 +4741,7 @@ 6E9B116622C5487100C22D81 /* DecisionServiceTests_UserProfiles.swift in Sources */, 6E34A6202319EBB800BAE302 /* Notifications.swift in Sources */, 6E75173322C520D400B2B157 /* Constants.swift in Sources */, + 8486181C286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift in Sources */, 6E7518A922C520D400B2B157 /* FeatureFlag.swift in Sources */, 84B4D75D27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E0A72D526C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift in Sources */, @@ -4756,6 +4775,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 */, @@ -4895,7 +4915,6 @@ 6E9B115622C5486E00C22D81 /* DecisionListenerTests.swift in Sources */, 6E9B114722C5486E00C22D81 /* OptimizelyErrorTests.swift in Sources */, 6E9B114B22C5486E00C22D81 /* BatchEventBuilderTest.swift in Sources */, - 84632F1128049F5C00F06D9C /* AudienceSegmentsHandlerTests.swift in Sources */, 6E9B115A22C5486E00C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B115422C5486E00C22D81 /* LoggerTests.swift in Sources */, 6E7518DF22C520D400B2B157 /* ConditionLeaf.swift in Sources */, @@ -4941,7 +4960,7 @@ 6E75184F22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E75190F22C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75193322C520D500B2B157 /* OPTDataStore.swift in Sources */, - 8428D3D5280753F900D0FB0C /* ZaiusApiManagerTests.swift in Sources */, + 84861811286D0B8900B7F41B /* ODPSegmentManagerTests.swift in Sources */, 6E7517EF22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E75194B22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75195722C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -4950,6 +4969,7 @@ 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 */, @@ -4964,6 +4984,7 @@ 6E9B114F22C5486E00C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E7518F722C520D500B2B157 /* UserAttribute.swift in Sources */, 6E75174522C520D400B2B157 /* HandlerRegistryService.swift in Sources */, + 8486181B286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift in Sources */, 6E8A3D492637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E981FC2232C363300FADDD6 /* DecisionListenerTests_Datafile.swift in Sources */, 6E9B114A22C5486E00C22D81 /* BucketTests_Others.swift in Sources */, @@ -4980,6 +5001,7 @@ 6E75177522C520D400B2B157 /* SDKVersion.swift in Sources */, 848617CD2863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, 6E75180722C520D400B2B157 /* DataStoreFile.swift in Sources */, + 8486181D286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift in Sources */, 6E75183722C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B115D22C5486E00C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */, 6E75173922C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -4998,6 +5020,7 @@ 6E9B115522C5486E00C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3825A3E6EA00999262 /* DecisionResponse.swift in Sources */, 848617DF2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, + 84861813286D0B8900B7F41B /* ODPManagerTests.swift in Sources */, 6E7516FD22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187322C520D400B2B157 /* Variation.swift in Sources */, 6E7517E322C520D400B2B157 /* DefaultDecisionService.swift in Sources */, @@ -5011,6 +5034,7 @@ 6E7518AF22C520D400B2B157 /* Group.swift in Sources */, 6EF8DE3524BF7D69008B9488 /* DecisionReasons.swift in Sources */, 84E2E96628540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, + 84861817286D0B8900B7F41B /* ODPEventManagerTests.swift in Sources */, 6E7517A522C520D400B2B157 /* Array+Extension.swift in Sources */, 6E9B115122C5486E00C22D81 /* NotificationCenterTests.swift in Sources */, 6E75184322C520D400B2B157 /* Event.swift in Sources */, diff --git a/Sources/Implementation/Events/BatchEventBuilder.swift b/Sources/Implementation/Events/BatchEventBuilder.swift index ae526235..6fd43173 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) diff --git a/Sources/ODP/LRUCache.swift b/Sources/ODP/LRUCache.swift index 836cb203..f18d2850 100644 --- a/Sources/ODP/LRUCache.swift +++ b/Sources/ODP/LRUCache.swift @@ -39,21 +39,15 @@ class LRUCache { let size: Int let timeoutInSecs: Int - #if DEBUG - let minSize = 1 - let minTimeoutInSecs = 1 - #else - let minSize = 10 - let minTimeoutInSecs = 60 - #endif - init(size: Int, timeoutInSecs: Int) { - self.size = max(size, minSize) - self.timeoutInSecs = max(timeoutInSecs, minTimeoutInSecs) + self.size = size + self.timeoutInSecs = timeoutInSecs self.reset() } func lookup(key: K) -> V? { + if size == 0 { return nil } + var element: CacheElement? = nil var needReset = false @@ -84,6 +78,8 @@ class LRUCache { } func save(key: K, value: V) { + if size == 0 { return } + queue.async(flags: .barrier) { let oldSegments = self.map[key] let newSegments = CacheElement(key: key, value: value) @@ -104,6 +100,8 @@ class LRUCache { // read cache contents without order update func peek(key: K) -> V? { + if size == 0 { return nil } + var element: CacheElement? = nil queue.sync { element = map[key] @@ -112,6 +110,8 @@ class LRUCache { } func reset() { + if size == 0 { return } + queue.sync { map = [K: CacheElement]() head = CacheElement() diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index aa498255..7b36d878 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -15,6 +15,7 @@ // import Foundation +import UIKit class ODPEventManager { let odpConfig: OptimizelyODPConfig @@ -27,9 +28,9 @@ class ODPEventManager { let logger = OPTLoggerFactory.getLogger() - init(sdkKey: String, odpConfig: OptimizelyODPConfig) { + init(sdkKey: String, odpConfig: OptimizelyODPConfig, apiManager: ZaiusRestApiManager? = nil) { self.odpConfig = odpConfig - self.zaiusMgr = ZaiusRestApiManager() + self.zaiusMgr = apiManager ?? ZaiusRestApiManager() self.queueLock = DispatchQueue(label: "event") @@ -70,6 +71,29 @@ class ODPEventManager { 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" + + "os": Utils.os, // "iOS", "Android", “Web” + "os_version": Utils.osVersion, // "13.2" + "device_type": Utils.deviceType, // "Phone", "Tablet", "TV", “PC” + "model": Utils.deviceModel // "iPhone 12", "Pixel 2 XL" + + // [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) { guard eventQueue.count < maxQueueSize else { let error = OptimizelyError.eventDispatchFailed("ODP EventQueue is full") @@ -81,17 +105,6 @@ class ODPEventManager { flush() } - func addCommonEventData(_ customData: [String: Any] = [:]) -> [String: Any] { - let commonData = [ - "source": "swift-sdk" - // others? - ] - - var data = customData - data.merge(commonData) { (current, _) in current } // keep custom data if conflicts - return data - } - func flush() { guard let odpApiKey = odpConfig.apiKey else { logger.d("ODP: event cannot be dispatched since apiKey not defined") diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index 15c2ea4b..35a5e05b 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -39,7 +39,7 @@ class ODPManager { } func fetchQualifiedSegments(userId: String, - segmentsToCheck: [String]? = nil, + segmentsToCheck: [String], options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { let userKey = vuidManager.isVuid(visitorId: userId) ? Constants.ODP.keyForVuid : Constants.ODP.keyForUserId @@ -56,7 +56,7 @@ class ODPManager { eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) } - func sendEvent(type: String, action: String, identifiers: [String: Any], data: [String: Any]) { + func sendEvent(type: String, action: String, identifiers: [String: String], data: [String: Any]) { eventManager.sendEvent(type: type, action: action, identifiers: identifiers, data: data) } @@ -70,7 +70,7 @@ class ODPManager { odpConfig.apiHost = apiHost // flush all ODP events waiting for apiKey - eventManager.flushEvents() + eventManager.flush() } var vuid: String { diff --git a/Sources/ODP/ODPSegmentManager.swift b/Sources/ODP/ODPSegmentManager.swift index 3b409b72..08208878 100644 --- a/Sources/ODP/ODPSegmentManager.swift +++ b/Sources/ODP/ODPSegmentManager.swift @@ -18,20 +18,22 @@ import Foundation class ODPSegmentManager { let odpConfig: OptimizelyODPConfig - let zaiusMgr = ZaiusGraphQLApiManager() + let zaiusMgr: ZaiusGraphQLApiManager let segmentsCache: LRUCache let logger = OPTLoggerFactory.getLogger() - init(odpConfig: OptimizelyODPConfig) { + init(odpConfig: OptimizelyODPConfig, apiManager: ZaiusGraphQLApiManager? = nil) { self.odpConfig = odpConfig + self.zaiusMgr = apiManager ?? ZaiusGraphQLApiManager() + self.segmentsCache = LRUCache(size: odpConfig.segmentsCacheSize, timeoutInSecs: odpConfig.segmentsCacheTimeoutInSecs) } func fetchQualifiedSegments(userKey: String, userValue: String, - segmentsToCheck: [String]? = nil, + segmentsToCheck: [String], options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { guard let odpApiKey = odpConfig.apiKey else { diff --git a/Sources/ODP/ZaiusGraphQLApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift index 09ab6988..9014f4e1 100644 --- a/Sources/ODP/ZaiusGraphQLApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -16,12 +16,9 @@ import Foundation -// MARK: - GraphQL API - // ODP GraphQL API // - https://api.zaius.com/v3/graphql - -// testODPApiKeyForAudienceSegments = "W4WzcEs-ABgXorzY7h1LCQ" +// - test ODP public API key = "W4WzcEs-ABgXorzY7h1LCQ" /* @@ -74,6 +71,31 @@ import Foundation } } } + + [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 ZaiusGraphQLApiManager { @@ -83,7 +105,7 @@ class ZaiusGraphQLApiManager { apiHost: String, userKey: String, userValue: String, - segmentsToCheck: [String]?, + segmentsToCheck: [String], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { let subsetFilter = makeSubsetFilter(segments: segmentsToCheck) @@ -108,26 +130,33 @@ class ZaiusGraphQLApiManager { defer { session.finishTasksAndInvalidate() } let task = session.dataTask(with: urlRequest) { data, _, error in - if let error = error { + guard error != nil, let data = data else { + let msg = error?.localizedDescription ?? "invalid data" self.logger.d { - "GraphQL download failed: \(error)" + "GraphQL download failed: \(msg)" } - completionHandler(nil, .fetchSegmentsFailed("download failed")) + completionHandler(nil, .fetchSegmentsFailed("network error")) return } - guard let data = data else { - completionHandler(nil, .fetchSegmentsFailed("response data empty")) + guard let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + completionHandler(nil, .fetchSegmentsFailed("decode error")) return } - guard let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") - else { - self.logger.d { - "Segments not in GraphQL response JSON (the user may be not registered yet): " + String(bytes: data, encoding: .utf8)! + if let odpErrors: [[String: Any]] = dict.extractComponent(keyPath: "errors") { + if let odpError = odpErrors.first, let errorClass: String = odpError.extractComponent(keyPath: "extension.classification") { + if errorClass == "InvalidIdentifierException" { + completionHandler(nil, .invalidSegmentIdentifier) + } else { + completionHandler(nil, .fetchSegmentsFailed(errorClass)) + } + return } - completionHandler(nil, .fetchSegmentsFailed("segments not in json")) + } + + guard let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") else { + completionHandler(nil, .fetchSegmentsFailed("decode error")) return } @@ -143,22 +172,14 @@ class ZaiusGraphQLApiManager { return URLSession(configuration: .ephemeral) } - func makeSubsetFilter(segments: [String]?) -> String { - // segments = nil: (fetch all segments) - // --> subsetFilter = "" + func makeSubsetFilter(segments: [String]) -> String { // segments = []: (fetch none) // --> subsetFilter = "(subset:[])" // segments = ["a"]: (fetch one segment) // --> subsetFilter = "(subset:[\"a\"])" - var subsetFilter = "" - - if let segments = segments { - let serial = segments.map { "\"\($0)\""}.joined(separator: ",") - subsetFilter = "(subset:[\(serial)])" - } - - return subsetFilter + let serial = segments.map { "\"\($0)\""}.joined(separator: ",") + return "(subset:[\(serial)])" } } diff --git a/Sources/ODP/ZaiusRestApiManager.swift b/Sources/ODP/ZaiusRestApiManager.swift index d154f5b1..81ca8018 100644 --- a/Sources/ODP/ZaiusRestApiManager.swift +++ b/Sources/ODP/ZaiusRestApiManager.swift @@ -16,7 +16,19 @@ import Foundation -// MARK: - REST API +// 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 ZaiusRestApiManager { diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index fa9cf7c9..d6d40d96 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -189,6 +189,9 @@ 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). diff --git a/Sources/Optimizely/OptimizelyError.swift b/Sources/Optimizely/OptimizelyError.swift index 0cc29e84..7eaf9b6b 100644 --- a/Sources/Optimizely/OptimizelyError.swift +++ b/Sources/Optimizely/OptimizelyError.swift @@ -82,6 +82,7 @@ public enum OptimizelyError: Error { // MARK: - AudienceSegements Errors + case invalidSegmentIdentifier case fetchSegmentsFailed(_ hint: String) case odpEventFailed(_ hint: String) } @@ -153,8 +154,9 @@ extension OptimizelyError: CustomStringConvertible, ReasonProtocol { case .eventDispatchFailed(let hint): message = "Event dispatch failed (\(hint))." case .eventDispatcherConfigError(let hint): message = "EventDispatcher config error (\(hint))." - case .fetchSegmentsFailed(let hint): message = "ODP: Audience segments fetch failed (\(hint))." - case .odpEventFailed(let hint): message = "ODP: Event send failed (\(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))." } return message diff --git a/Sources/Utils/Utils.swift b/Sources/Utils/Utils.swift index fc285429..68b9150c 100644 --- a/Sources/Utils/Utils.swift +++ b/Sources/Utils/Utils.swift @@ -15,12 +15,27 @@ // import Foundation +import UIKit class Utils { // from auto-generated variable OPTIMIZELYSDKVERSION static var sdkVersion: String = OPTIMIZELYSDKVERSION - + static let swiftSdkClientName = "swift-sdk" + + static let os = UIDevice.current.systemName + static let osVersion = UIDevice.current.systemVersion + static let deviceModel = UIDevice.current.model + static var deviceType: String { + switch UIDevice.current.userInterfaceIdiom { + case .phone: return "Phone" + case .pad: return "Tablet" + case .tv: return "TV" + case .mac: return "PC" + default: return "Others" + } + } + private static let jsonEncoder = JSONEncoder() // @objc NSNumber can be casted either Bool, Int, or Double 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-Common/ODPEventManagerTests.swift b/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift new file mode 100644 index 00000000..ffe3b922 --- /dev/null +++ b/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift @@ -0,0 +1,21 @@ +// +// 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 { + +} diff --git a/Tests/OptimizelyTests-Common/ODPManagerTests.swift b/Tests/OptimizelyTests-Common/ODPManagerTests.swift new file mode 100644 index 00000000..c32c0963 --- /dev/null +++ b/Tests/OptimizelyTests-Common/ODPManagerTests.swift @@ -0,0 +1,21 @@ +// +// 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 { + +} diff --git a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests copy.swift similarity index 83% rename from Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift rename to Tests/OptimizelyTests-Common/ODPSegmentManagerTests copy.swift index cd5c33d5..e2739c8d 100644 --- a/Tests/OptimizelyTests-Common/ZaiusApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests copy.swift @@ -16,7 +16,7 @@ import XCTest -class ZaiusApiManagerTests: XCTestCase { +class ODPSegmentManagerTests: XCTestCase { let userKey = "vuid" let userValue = "test-user-value" @@ -29,14 +29,18 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodResponseData)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + manager.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in XCTAssertNil(error) XCTAssertEqual(segments, ["qualified"]) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - guard let request = ZaiusApiManagerTests.createdApiRequest else { + guard let request = ODPSegmentManagerTests.createdApiRequest else { XCTFail() return } @@ -56,7 +60,11 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodEmptyResponseData)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + manager.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in XCTAssertNil(error) XCTAssertEqual(segments, []) sem.signal() @@ -68,7 +76,11 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + manager.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in if case .fetchSegmentsFailed("segments not in json") = error { XCTAssert(true) } else { @@ -84,7 +96,11 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: nil) { segments, error in + manager.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in if case .fetchSegmentsFailed("download failed") = error { XCTAssert(true) } else { @@ -100,12 +116,16 @@ class ZaiusApiManagerTests: XCTestCase { let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) let sem = DispatchSemaphore(value: 0) - manager.fetch(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, segmentsToCheck: ["a", "b"]) { _, _ in + manager.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: ["a", "b"]) { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - guard let request = ZaiusApiManagerTests.createdApiRequest else { + guard let request = ODPSegmentManagerTests.createdApiRequest else { XCTFail() return } @@ -118,7 +138,7 @@ class ZaiusApiManagerTests: XCTestCase { } func testMakeSubsetFilter() { - let manager = ZaiusApiManager() + let manager = ZaiusGraphQLApiManager() XCTAssertEqual("", manager.makeSubsetFilter(segments: nil)) XCTAssertEqual("(subset:[])", manager.makeSubsetFilter(segments: [])) @@ -137,7 +157,7 @@ class ZaiusApiManagerTests: XCTestCase { // MARK: - MockZaiusApiManager - class MockZaiusApiManager: ZaiusApiManager { + class MockZaiusApiManager: ZaiusGraphQLApiManager { let mockUrlSession: URLSession init(_ urlSession: URLSession) { @@ -177,7 +197,7 @@ class ZaiusApiManagerTests: XCTestCase { } override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - ZaiusApiManagerTests.createdApiRequest = request + ODPSegmentManagerTests.createdApiRequest = request return MockDataTask() { let statusCode = self.statusCode != 0 ? self.statusCode : 200 diff --git a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift similarity index 60% rename from Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift rename to Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift index 606612da..e44a92e2 100644 --- a/Tests/OptimizelyTests-Common/AudienceSegmentsHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift @@ -16,8 +16,10 @@ import XCTest -class ODPManagerTests: XCTestCase { - var handler = ODPManager(odpConfig: OptimizelyODPConfig(segmentsCacheSize: 100, segmentsCacheTimeoutInSecs: 100)) +class ODPSegmentManagerTests: XCTestCase { + var manager: ODPSegmentManager! + var odpConfig = OptimizelyODPConfig() + var options = [OptimizelySegmentOption]() var apiKey = "valid" @@ -27,7 +29,8 @@ class ODPManagerTests: XCTestCase { var userValue = "test-user" override func setUp() { - handler.zaiusMgr = MockZaiusApiManager() + manager = ODPSegmentManager(odpConfig: odpConfig, + apiManager: MockZaiusApiManager()) } func testSuccess_cacheMiss() { @@ -35,8 +38,10 @@ class ODPManagerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: options) { segments, error in + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + segmentsToCheck: [], + options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["new-customer"], segments) sem.signal() @@ -50,8 +55,10 @@ class ODPManagerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: options) { segments, error in + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + segmentsToCheck: [], + options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["a"], segments) sem.signal() @@ -63,8 +70,12 @@ class ODPManagerTests: XCTestCase { func testError() { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: "invalid-key", apiHost: apiHost, userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: []) { segments, error in + odpConfig.apiKey = "invalid-key" + + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + segmentsToCheck: [], + options: []) { segments, error in XCTAssertNotNil(error) XCTAssert(segments!.isEmpty ) sem.signal() @@ -81,8 +92,10 @@ class ODPManagerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: options) { segments, error in + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + segmentsToCheck: [], + 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") @@ -100,8 +113,10 @@ class ODPManagerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) - handler.fetchQualifiedSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, - segmentsToCheck: nil, options: options) { segments, error in + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + segmentsToCheck: [], + options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["new-customer"], segments, "cache lookup should be skipped") XCTAssertEqual(segments, self.peekCache(self.userKey, self.userValue)) @@ -115,28 +130,29 @@ class ODPManagerTests: XCTestCase { // MARK: - Utils func setCache(_ userKey: String, _ userValue: String, _ value: [String]) { - let cacheKey = handler.makeCacheKey(userKey, userValue) - handler.segmentsCache.save(key: cacheKey, value: value) + let cacheKey = manager.makeCacheKey(userKey, userValue) + manager.segmentsCache.save(key: cacheKey, value: value) } func peekCache(_ userKey: String, _ userValue: String) -> [String]? { - let cacheKey = handler.makeCacheKey(userKey, userValue) - return handler.segmentsCache.peek(key: cacheKey) + let cacheKey = manager.makeCacheKey(userKey, userValue) + return manager.segmentsCache.peek(key: cacheKey) } var cacheCount: Int { - return handler.segmentsCache.map.count + return manager.segmentsCache.map.count } - // MARK: - MockZaiusApiManager + // MARK: - MockSegmentManager - class MockZaiusApiManager: ZaiusApiManager { - override func fetch(apiKey: String, - apiHost: String, - userKey: String, - userValue: String, - segmentsToCheck: [String]?, - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + class MockZaiusApiManager: ZaiusGraphQLApiManager { + + override func fetchSegments(apiKey: String, + apiHost: String, + userKey: String, + userValue: String, + segmentsToCheck: [String], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { DispatchQueue.global().async { if apiKey == "invalid-key" { completionHandler([], OptimizelyError.fetchSegmentsFailed("invalid key")) @@ -148,4 +164,3 @@ class ODPManagerTests: XCTestCase { } } - diff --git a/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift b/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift new file mode 100644 index 00000000..af9d1d32 --- /dev/null +++ b/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift @@ -0,0 +1,21 @@ +// +// 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 { + +} diff --git a/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift b/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift new file mode 100644 index 00000000..861b0215 --- /dev/null +++ b/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift @@ -0,0 +1,292 @@ +// +// 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 ZaiusGraphQLApiManagerTests: XCTestCase { + let userKey = "vuid" + let userValue = "test-user-value" + let apiKey = "test-api-key" + let apiHost = "https://test-host" + + static var createdApiRequest: URLRequest? + + func testFetchQualifiedSegments_success() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodResponseData)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, ["a"]) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + guard let request = ZaiusGraphQLApiManagerTests.createdApiRequest else { + XCTFail() + return + } + + let expectedBody = [ + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name state}}}}}" + ] + + XCTAssertEqual(apiHost + "/v3/graphql", request.url?.absoluteString) + XCTAssertEqual("POST", request.httpMethod) + XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) + XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) + XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) + } + + func testFetchQualifiedSegments_successWithEmptySegments() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodEmptyResponseData)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, []) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_badResponse() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + if case .fetchSegmentsFailed("segments not in json") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testFetchQualifiedSegments_networkError() { + let api = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: []) { segments, error in + if case .fetchSegmentsFailed("download failed") = error { + XCTAssert(true) + } else { + XCTFail() + } + XCTAssertNil(segments) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testGraphQLRequest_subsetSegments() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) + + let sem = DispatchSemaphore(value: 0) + api.fetchSegments(apiKey: apiKey, + apiHost: apiHost, + userKey: userKey, + userValue: userValue, + segmentsToCheck: ["a", "b"]) { _, _ in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + guard let request = ZaiusGraphQLApiManagerTests.createdApiRequest else { + XCTFail() + return + } + + let expectedBody = [ + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences(subset:[\"a\",\"b\"]) {edges {node {name state}}}}}" + ] + + XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) + } + + func testMakeSubsetFilter() { + let api = ZaiusGraphQLApiManager() + + XCTAssertEqual("(subset:[])", api.makeSubsetFilter(segments: [])) + XCTAssertEqual("(subset:[\"a\"])", api.makeSubsetFilter(segments: ["a"])) + XCTAssertEqual("(subset:[\"a\",\"b\",\"c\"])",api.makeSubsetFilter(segments: ["a", "b", "c"])) + } + + 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: - MockZaiusApiManager + + class MockZaiusApiManager: ZaiusGraphQLApiManager { + let mockUrlSession: URLSession + + init(_ urlSession: URLSession) { + mockUrlSession = urlSession + } + + override func getSession() -> URLSession { + return mockUrlSession + } + } + + // MARK: - MockZaiusUrlSession + + class MockZaiusUrlSession: 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 ?? MockZaiusUrlSession.goodResponseData + } + + override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + ZaiusGraphQLApiManagerTests.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 goodErrorResponseData: 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 badResponseData: String = """ + { + "data": {} + } + """ + + } + +} diff --git a/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift b/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift new file mode 100644 index 00000000..78bfbf7e --- /dev/null +++ b/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift @@ -0,0 +1,269 @@ +// +// 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 ZaiusRestApiManagerTests: XCTestCase { +// let userKey = "vuid" +// +// let userValue = "test-user-value" +// let apiKey = "test-api-key" +// let apiHost = "https://test-host" +// +// static var createdApiRequest: URLRequest? +// +// func testFetchQualifiedSegments_success() { +// let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodResponseData)) +// +// let sem = DispatchSemaphore(value: 0) +// manager.fetchSegments(apiKey: apiKey, +// apiHost: apiHost, +// userKey: userKey, +// userValue: userValue, +// segmentsToCheck: []) { segments, error in +// XCTAssertNil(error) +// XCTAssertEqual(segments, ["qualified"]) +// sem.signal() +// } +// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) +// +// guard let request = ODPSegmentManagerTests.createdApiRequest else { +// XCTFail() +// return +// } +// +// let expectedBody = [ +// "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name state}}}}}" +// ] +// +// XCTAssertEqual(apiHost + "/v3/graphql", request.url?.absoluteString) +// XCTAssertEqual("POST", request.httpMethod) +// XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) +// XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) +// XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) +// } +// +// func testFetchQualifiedSegments_successWithEmptySegments() { +// let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodEmptyResponseData)) +// +// let sem = DispatchSemaphore(value: 0) +// manager.fetchSegments(apiKey: apiKey, +// apiHost: apiHost, +// userKey: userKey, +// userValue: userValue, +// segmentsToCheck: []) { segments, error in +// XCTAssertNil(error) +// XCTAssertEqual(segments, []) +// sem.signal() +// } +// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) +// } +// +// func testFetchQualifiedSegments_badResponse() { +// let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) +// +// let sem = DispatchSemaphore(value: 0) +// manager.fetchSegments(apiKey: apiKey, +// apiHost: apiHost, +// userKey: userKey, +// userValue: userValue, +// segmentsToCheck: []) { segments, error in +// if case .fetchSegmentsFailed("segments not in json") = error { +// XCTAssert(true) +// } else { +// XCTFail() +// } +// XCTAssertNil(segments) +// sem.signal() +// } +// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) +// } +// +// func testFetchQualifiedSegments_networkError() { +// let manager = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) +// +// let sem = DispatchSemaphore(value: 0) +// manager.fetchSegments(apiKey: apiKey, +// apiHost: apiHost, +// userKey: userKey, +// userValue: userValue, +// segmentsToCheck: []) { segments, error in +// if case .fetchSegmentsFailed("download failed") = error { +// XCTAssert(true) +// } else { +// XCTFail() +// } +// XCTAssertNil(segments) +// sem.signal() +// } +// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) +// } +// +// func testGraphQLRequest_subsetSegments() { +// let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) +// +// let sem = DispatchSemaphore(value: 0) +// manager.fetchSegments(apiKey: apiKey, +// apiHost: apiHost, +// userKey: userKey, +// userValue: userValue, +// segmentsToCheck: ["a", "b"]) { _, _ in +// sem.signal() +// } +// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) +// +// guard let request = ODPSegmentManagerTests.createdApiRequest else { +// XCTFail() +// return +// } +// +// let expectedBody = [ +// "query": "query {customer(\(userKey): \"\(userValue)\") {audiences(subset:[\"a\",\"b\"]) {edges {node {name state}}}}}" +// ] +// +// XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) +// } +// +// func testMakeSubsetFilter() { +// let manager = ZaiusGraphQLApiManager() +// +// XCTAssertEqual("", manager.makeSubsetFilter(segments: nil)) +// XCTAssertEqual("(subset:[])", manager.makeSubsetFilter(segments: [])) +// XCTAssertEqual("(subset:[\"a\"])", manager.makeSubsetFilter(segments: ["a"])) +// XCTAssertEqual("(subset:[\"a\",\"b\",\"c\"])",manager.makeSubsetFilter(segments: ["a", "b", "c"])) +// } +// +// 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: - MockZaiusApiManager +// +// class MockZaiusApiManager: ZaiusGraphQLApiManager { +// let mockUrlSession: URLSession +// +// init(_ urlSession: URLSession) { +// mockUrlSession = urlSession +// } +// +// override func getSession() -> URLSession { +// return mockUrlSession +// } +// } +// +// // MARK: - MockZaiusUrlSession +// +// class MockZaiusUrlSession: 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 ?? MockZaiusUrlSession.goodResponseData +// } +// +// override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { +// ODPSegmentManagerTests.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": "qualified", +// "state": "qualified", +// "description": "qualifed sample" +// } +// }, +// { +// "node": { +// "name": "not-qualified", +// "state": "not_qualified", +// "description": "not-qualified sample" +// } +// } +// ] +// } +// } +// } +// } +// """ +// +// static let goodEmptyResponseData: String = """ +// { +// "data": { +// "customer": { +// "audiences": { +// "edges": [] +// } +// } +// } +// } +// """ +// +// static let badResponseData: String = """ +// { +// "data": {} +// } +// """ +// +// } +// +//} diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index 094e6fcb..61c97893 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -18,8 +18,8 @@ import XCTest class OptimizelyUserContextTests_Segments: XCTestCase { - var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - var odpManager = MockODPManager(cacheSize: 100, cacheTimeoutInSecs: 100) + var optimizely: OptimizelyClient! + var odpConfig: OptimizelyODPConfig! var user: OptimizelyUserContext! let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! @@ -30,7 +30,11 @@ class OptimizelyUserContextTests_Segments: XCTestCase { let kUserValue = "custom_id_value" override func setUp() { - optimizely.odpManager = odpManager + odpConfig = OptimizelyODPConfig(segmentsCacheSize: 100, + segmentsCacheTimeoutInSecs: 100, + apiHost: kApiHost, + apiKey: kApiKey) + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, odpConfig: odpConfig) user = optimizely.createUserContext(userId: kUserId) } @@ -51,7 +55,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_successDefaultUser() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { segments, error in + user.fetchQualifiedSegments { segments, error in XCTAssertNil(error) XCTAssert(segments == ["segment-1"]) XCTAssert(self.user.qualifiedSegments == segments) @@ -66,7 +70,7 @@ class OptimizelyUserContextTests_Segments: XCTestCase { user.optimizely = nil let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { segments, error in + user.fetchQualifiedSegments { segments, error in XCTAssertEqual(OptimizelyError.sdkNotReady.reason, error?.reason) XCTAssertNil(segments) XCTAssertNil(self.user.qualifiedSegments) @@ -77,7 +81,10 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_fetchFailed() { let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: "invalid-key", apiHost: kApiHost) { segments, error in + + odpConfig.apiKey = "invalid-key" + + user.fetchQualifiedSegments { segments, error in XCTAssertNotNil(error) XCTAssertNil(segments) XCTAssertNil(self.user.qualifiedSegments) @@ -88,22 +95,12 @@ class OptimizelyUserContextTests_Segments: XCTestCase { // MARK: - SegmentsToCheck - func testFetchQualifiedSegments_segmentsToCheck_emptyBeforeStart() { - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { _, _ in - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - - XCTAssertNil(segmentHandler.segmentsToCheck) - } - func testFetchQualifiedSegments_segmentsToCheck_validAfterStart() { let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! try? optimizely.start(datafile: datafile) let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { _, _ in + user.fetchQualifiedSegments { _, _ in sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -231,30 +228,16 @@ extension OptimizelyUserContextTests_Segments { // MARK: - MockODPManager class MockODPManager: ODPManager { - var apiKey: String? - var apiHost: String? - var userKey: String? - var userValue: String? - var segmentsToCheck: [String]? - var options: [OptimizelySegmentOption]? - - override func fetchQualifiedSegments(apiKey: String, - apiHost: String, - userKey: String, - userValue: String, - segmentsToCheck: [String]?, + var segmentsToCheck: [String]! + + override func fetchQualifiedSegments(userId: String, + segmentsToCheck: [String], options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - - self.apiKey = apiKey - self.apiHost = apiHost - self.userKey = userKey - self.userValue = userValue self.segmentsToCheck = segmentsToCheck - self.options = options - + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - if apiKey == "invalid-key" { + if self.odpConfig.apiKey == "invalid-key" { completionHandler(nil, OptimizelyError.generic) } else { let sampleSegments = ["segment-1"] From 3cbbdbf480ca6151a49688da27468e644ab9e729 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 Jul 2022 10:38:21 -0700 Subject: [PATCH 47/84] error handling for odp events --- Sources/ODP/ODPEventManager.swift | 36 +++++++++++++------- Sources/ODP/ZaiusRestApiManager.swift | 43 ++++++++++++++++-------- Sources/Optimizely/OptimizelyError.swift | 4 +-- Sources/Utils/Utils.swift | 4 +-- 4 files changed, 56 insertions(+), 31 deletions(-) diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index 7b36d878..3ff4b24a 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -79,9 +79,9 @@ class ODPEventManager { "data_source": Utils.swiftSdkClientName, // "swift-sdk" "data_source_version": Utils.sdkVersion, // "3.10.2" - "os": Utils.os, // "iOS", "Android", “Web” - "os_version": Utils.osVersion, // "13.2" - "device_type": Utils.deviceType, // "Phone", "Tablet", "TV", “PC” + "os": Utils.os, // ("iOS", "Android", “Web”) + "os_version": Utils.osVersion, // "13.2", ... + "device_type": Utils.deviceType, // ("Phone", "Tablet", "Smart TV", “PC”, "Other") "model": Utils.deviceModel // "iPhone 12", "Pixel 2 XL" // [optional] @@ -144,20 +144,30 @@ class ODPEventManager { self.zaiusMgr.sendODPEvents(apiKey: odpApiKey, apiHost: self.odpConfig.apiHost, events: events) { error in - if error != nil { - self.logger.e(error!.reason) - failureCount += 1 - } else { - removeStoredEvents(num: numEvents) - failureCount = 0 + defer { + notify.leave() // our send is done. } - // our send is done. - notify.leave() + if let error = error { + self.logger.e(error.reason) + + // retry only if needed (non-permanent) + if case .odpEventFailed(_, let canRetry) = error { + if canRetry { + failureCount += 1 + return + } else { + // permanent errors (400 response or invalid events, etc) + // discard these events so not they do not block following valid events + } + } + } + + removeStoredEvents(num: numEvents) + failureCount = 0 } - // wait for send - notify.wait() + notify.wait() // wait for send completed } } } diff --git a/Sources/ODP/ZaiusRestApiManager.swift b/Sources/ODP/ZaiusRestApiManager.swift index 81ca8018..9219fa95 100644 --- a/Sources/ODP/ZaiusRestApiManager.swift +++ b/Sources/ODP/ZaiusRestApiManager.swift @@ -37,12 +37,14 @@ class ZaiusRestApiManager { events: [ODPEvent], completionHandler: @escaping (OptimizelyError?) -> Void) { guard let url = URL(string: "\(apiHost)/v3/events") else { - completionHandler(.odpEventFailed("Invalid url")) + let canRetry = false + completionHandler(.odpEventFailed("Invalid url", canRetry)) return } guard let body = try? JSONSerialization.data(withJSONObject: events.map{ $0.dict }) else { - completionHandler(.odpEventFailed("Invalid JSON")) + let canRetry = false + completionHandler(.odpEventFailed("Invalid JSON", canRetry)) return } @@ -57,23 +59,36 @@ class ZaiusRestApiManager { 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 { - completionHandler(.odpEventFailed(error.localizedDescription)) + errMessage = error.localizedDescription return } - if let response = response as? HTTPURLResponse { - if response.statusCode >= 400 { - var message = "UNKNOWN" - if let data = data, let msg = String(bytes: data, encoding: .utf8) { - message = msg - } - completionHandler(.odpEventFailed(message)) - return - } + guard let response = response as? HTTPURLResponse else { + errMessage = "invalid response" + return + } + + switch response.statusCode { + case ..<400: + errMessage = nil // success + case 400..<500: + errMessage = "\(response.statusCode)" + canRetry = false // no retry (client error) + default: + errMessage = "\(response.statusCode)" } - - completionHandler(nil) } task.resume() diff --git a/Sources/Optimizely/OptimizelyError.swift b/Sources/Optimizely/OptimizelyError.swift index 7eaf9b6b..30acfdf4 100644 --- a/Sources/Optimizely/OptimizelyError.swift +++ b/Sources/Optimizely/OptimizelyError.swift @@ -84,7 +84,7 @@ public enum OptimizelyError: Error { case invalidSegmentIdentifier case fetchSegmentsFailed(_ hint: String) - case odpEventFailed(_ hint: String) + case odpEventFailed(_ hint: String, _ canRetry: Bool) } // MARK: - CustomStringConvertible @@ -156,7 +156,7 @@ extension OptimizelyError: CustomStringConvertible, ReasonProtocol { 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 .odpEventFailed(let hint, _): message = "ODP event send failed (\(hint))." } return message diff --git a/Sources/Utils/Utils.swift b/Sources/Utils/Utils.swift index 68b9150c..8284ede5 100644 --- a/Sources/Utils/Utils.swift +++ b/Sources/Utils/Utils.swift @@ -30,9 +30,9 @@ class Utils { switch UIDevice.current.userInterfaceIdiom { case .phone: return "Phone" case .pad: return "Tablet" - case .tv: return "TV" + case .tv: return "Smart TV" case .mac: return "PC" - default: return "Others" + default: return "Other" } } From 6c348602ad3a0a7824618292b8d00b15ec5996b7 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 Jul 2022 11:04:45 -0700 Subject: [PATCH 48/84] fix odp event format --- Sources/ODP/ODPEventManager.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index 3ff4b24a..f988a01d 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -79,10 +79,11 @@ class ODPEventManager { "data_source": Utils.swiftSdkClientName, // "swift-sdk" "data_source_version": Utils.sdkVersion, // "3.10.2" - "os": Utils.os, // ("iOS", "Android", “Web”) + // [optional] client sdks only + "os": Utils.os, // ("iOS", "Android", "Mac OS", "Windows", "Linux", ...) "os_version": Utils.osVersion, // "13.2", ... - "device_type": Utils.deviceType, // ("Phone", "Tablet", "Smart TV", “PC”, "Other") - "model": Utils.deviceModel // "iPhone 12", "Pixel 2 XL" + "device_type": Utils.deviceType, // fixed set = ("Phone", "Tablet", "Smart TV", “PC”, "Other") + "model": Utils.deviceModel // ("iPhone 12", "iPad 2", "Pixel 2", "SM-A515F", ...) // [optional] // "data_source_instance": , // if need subtypes of data_source From 5363800ee4f7d55f3c9826886f36ead59377a621 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 Jul 2022 14:48:47 -0700 Subject: [PATCH 49/84] add tests for graphql and events apis --- .../ODPZaiusGraphQLApiManagerTests.swift | 121 +++-- .../ODPZaiusRestApiManagerTests.swift | 413 +++++++----------- Tests/TestUtils/OTUtils.swift | 10 + 3 files changed, 255 insertions(+), 289 deletions(-) diff --git a/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift b/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift index 861b0215..b8052dbd 100644 --- a/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift @@ -20,40 +20,50 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { let userKey = "vuid" let userValue = "test-user-value" let apiKey = "test-api-key" - let apiHost = "https://test-host" + let apiHost = "test-host" static var createdApiRequest: URLRequest? - func testFetchQualifiedSegments_success() { + // MARK: - Request + + func testFetchQualifiedSegments_validRequest() { let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodResponseData)) - let sem = DispatchSemaphore(value: 0) api.fetchSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, - segmentsToCheck: []) { segments, error in - XCTAssertNil(error) - XCTAssertEqual(segments, ["a"]) - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - - guard let request = ZaiusGraphQLApiManagerTests.createdApiRequest else { - XCTFail() - return - } + segmentsToCheck: ["a", "b", "c"]) {_,_ in } + let request = ZaiusGraphQLApiManagerTests.createdApiRequest! let expectedBody = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name state}}}}}" + "query": "query {customer(\(userKey): \"\(userValue)\") {audiences(subset:[\"a\",\"b\",\"c\"]) {edges {node {name state}}}}}" ] - + XCTAssertEqual(apiHost + "/v3/graphql", request.url?.absoluteString) XCTAssertEqual("POST", request.httpMethod) XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) } + + // MARK: - Success + + func testFetchQualifiedSegments_success() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.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 = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodEmptyResponseData)) @@ -63,7 +73,7 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { apiHost: apiHost, userKey: userKey, userValue: userValue, - segmentsToCheck: []) { segments, error in + segmentsToCheck: ["a", "b", "c"]) { segments, error in XCTAssertNil(error) XCTAssertEqual(segments, []) sem.signal() @@ -71,8 +81,10 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - func testFetchQualifiedSegments_badResponse() { - let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) + // MARK: - Failure + + func testFetchQualifiedSegments_invalidIdentifier() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.invalidIdentifierResponseData)) let sem = DispatchSemaphore(value: 0) api.fetchSegments(apiKey: apiKey, @@ -80,7 +92,7 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { userKey: userKey, userValue: userValue, segmentsToCheck: []) { segments, error in - if case .fetchSegmentsFailed("segments not in json") = error { + if case .invalidSegmentIdentifier = error { XCTAssert(true) } else { XCTFail() @@ -90,9 +102,9 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - - func testFetchQualifiedSegments_networkError() { - let api = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) + + func testFetchQualifiedSegments_otherException() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.otherExceptionResponseData)) let sem = DispatchSemaphore(value: 0) api.fetchSegments(apiKey: apiKey, @@ -100,7 +112,7 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { userKey: userKey, userValue: userValue, segmentsToCheck: []) { segments, error in - if case .fetchSegmentsFailed("download failed") = error { + if case .invalidSegmentIdentifier = error { XCTAssert(true) } else { XCTFail() @@ -110,32 +122,49 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - - func testGraphQLRequest_subsetSegments() { - let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) + + func testFetchQualifiedSegments_badResponse() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) let sem = DispatchSemaphore(value: 0) api.fetchSegments(apiKey: apiKey, apiHost: apiHost, userKey: userKey, userValue: userValue, - segmentsToCheck: ["a", "b"]) { _, _ in + 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_networkError() { + let api = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) - guard let request = ZaiusGraphQLApiManagerTests.createdApiRequest else { - XCTFail() - return + 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() } - - let expectedBody = [ - "query": "query {customer(\(userKey): \"\(userValue)\") {audiences(subset:[\"a\",\"b\"]) {edges {node {name state}}}}}" - ] - - XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } + // MARK: - Others + func testMakeSubsetFilter() { let api = ZaiusGraphQLApiManager() @@ -256,7 +285,7 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { } """ - static let goodErrorResponseData: String = """ + static let invalidIdentifierResponseData: String = """ { "errors": [ { @@ -281,6 +310,22 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { } """ + 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/ODPZaiusRestApiManagerTests.swift b/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift index 78bfbf7e..c8050af7 100644 --- a/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift @@ -16,254 +16,165 @@ import XCTest -//class ZaiusRestApiManagerTests: XCTestCase { -// let userKey = "vuid" -// -// let userValue = "test-user-value" -// let apiKey = "test-api-key" -// let apiHost = "https://test-host" -// -// static var createdApiRequest: URLRequest? -// -// func testFetchQualifiedSegments_success() { -// let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodResponseData)) -// -// let sem = DispatchSemaphore(value: 0) -// manager.fetchSegments(apiKey: apiKey, -// apiHost: apiHost, -// userKey: userKey, -// userValue: userValue, -// segmentsToCheck: []) { segments, error in -// XCTAssertNil(error) -// XCTAssertEqual(segments, ["qualified"]) -// sem.signal() -// } -// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) -// -// guard let request = ODPSegmentManagerTests.createdApiRequest else { -// XCTFail() -// return -// } -// -// let expectedBody = [ -// "query": "query {customer(\(userKey): \"\(userValue)\") {audiences {edges {node {name state}}}}}" -// ] -// -// XCTAssertEqual(apiHost + "/v3/graphql", request.url?.absoluteString) -// XCTAssertEqual("POST", request.httpMethod) -// XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) -// XCTAssertEqual("application/json", request.value(forHTTPHeaderField: "Content-Type")) -// XCTAssertEqual(apiKey, request.value(forHTTPHeaderField: "x-api-key")) -// } -// -// func testFetchQualifiedSegments_successWithEmptySegments() { -// let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.goodEmptyResponseData)) -// -// let sem = DispatchSemaphore(value: 0) -// manager.fetchSegments(apiKey: apiKey, -// apiHost: apiHost, -// userKey: userKey, -// userValue: userValue, -// segmentsToCheck: []) { segments, error in -// XCTAssertNil(error) -// XCTAssertEqual(segments, []) -// sem.signal() -// } -// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) -// } -// -// func testFetchQualifiedSegments_badResponse() { -// let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.badResponseData)) -// -// let sem = DispatchSemaphore(value: 0) -// manager.fetchSegments(apiKey: apiKey, -// apiHost: apiHost, -// userKey: userKey, -// userValue: userValue, -// segmentsToCheck: []) { segments, error in -// if case .fetchSegmentsFailed("segments not in json") = error { -// XCTAssert(true) -// } else { -// XCTFail() -// } -// XCTAssertNil(segments) -// sem.signal() -// } -// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) -// } -// -// func testFetchQualifiedSegments_networkError() { -// let manager = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) -// -// let sem = DispatchSemaphore(value: 0) -// manager.fetchSegments(apiKey: apiKey, -// apiHost: apiHost, -// userKey: userKey, -// userValue: userValue, -// segmentsToCheck: []) { segments, error in -// if case .fetchSegmentsFailed("download failed") = error { -// XCTAssert(true) -// } else { -// XCTFail() -// } -// XCTAssertNil(segments) -// sem.signal() -// } -// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) -// } -// -// func testGraphQLRequest_subsetSegments() { -// let manager = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200)) -// -// let sem = DispatchSemaphore(value: 0) -// manager.fetchSegments(apiKey: apiKey, -// apiHost: apiHost, -// userKey: userKey, -// userValue: userValue, -// segmentsToCheck: ["a", "b"]) { _, _ in -// sem.signal() -// } -// XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) -// -// guard let request = ODPSegmentManagerTests.createdApiRequest else { -// XCTFail() -// return -// } -// -// let expectedBody = [ -// "query": "query {customer(\(userKey): \"\(userValue)\") {audiences(subset:[\"a\",\"b\"]) {edges {node {name state}}}}}" -// ] -// -// XCTAssertEqual(expectedBody, try! JSONDecoder().decode([String: String].self, from: request.httpBody!)) -// } -// -// func testMakeSubsetFilter() { -// let manager = ZaiusGraphQLApiManager() -// -// XCTAssertEqual("", manager.makeSubsetFilter(segments: nil)) -// XCTAssertEqual("(subset:[])", manager.makeSubsetFilter(segments: [])) -// XCTAssertEqual("(subset:[\"a\"])", manager.makeSubsetFilter(segments: ["a"])) -// XCTAssertEqual("(subset:[\"a\",\"b\",\"c\"])",manager.makeSubsetFilter(segments: ["a", "b", "c"])) -// } -// -// 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: - MockZaiusApiManager -// -// class MockZaiusApiManager: ZaiusGraphQLApiManager { -// let mockUrlSession: URLSession -// -// init(_ urlSession: URLSession) { -// mockUrlSession = urlSession -// } -// -// override func getSession() -> URLSession { -// return mockUrlSession -// } -// } -// -// // MARK: - MockZaiusUrlSession -// -// class MockZaiusUrlSession: 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 ?? MockZaiusUrlSession.goodResponseData -// } -// -// override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { -// ODPSegmentManagerTests.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": "qualified", -// "state": "qualified", -// "description": "qualifed sample" -// } -// }, -// { -// "node": { -// "name": "not-qualified", -// "state": "not_qualified", -// "description": "not-qualified sample" -// } -// } -// ] -// } -// } -// } -// } -// """ -// -// static let goodEmptyResponseData: String = """ -// { -// "data": { -// "customer": { -// "audiences": { -// "edges": [] -// } -// } -// } -// } -// """ -// -// static let badResponseData: String = """ -// { -// "data": {} -// } -// """ -// -// } -// -//} +class ZaiusRestApiManagerTests: XCTestCase { + let userKey = "vuid" + let userValue = "test-user-value" + let apiKey = "test-api-key" + let apiHost = "test-host" + + let events: [ODPEvent] = [ + ODPEvent(type: "fullstack", action: "a", identifiers: ["id-key-1": "id-value-1"], data: ["key-1": "value-1"]) + ] + + static var createdApiRequest: URLRequest? + + func testSendODPEvents_validRequest() { + let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, + responseData: MockZaiusUrlSession.successResponseData)) + api.sendODPEvents(apiKey: apiKey, apiHost: apiHost, events: events) { _ in } + + let request = ZaiusRestApiManagerTests.createdApiRequest! + + 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.. URLSession { + return mockUrlSession + } + } + + // MARK: - MockZaiusUrlSession + + class MockZaiusUrlSession: 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 ?? MockZaiusUrlSession.successResponseData + } + + override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + ZaiusRestApiManagerTests.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 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/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index 3a035492..4921929e 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(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)) } From 77f2ee71b9bdf2f856751efb45a17cbd7fc42cf1 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 Jul 2022 15:53:10 -0700 Subject: [PATCH 50/84] fix tests for ODPSegmentManager --- Sources/ODP/ZaiusGraphQLApiManager.swift | 12 ++- .../ODPSegmentManagerTests.swift | 81 +++++++++++++------ .../ODPZaiusGraphQLApiManagerTests.swift | 40 +++++++++ 3 files changed, 107 insertions(+), 26 deletions(-) diff --git a/Sources/ODP/ZaiusGraphQLApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift index 9014f4e1..3a86f150 100644 --- a/Sources/ODP/ZaiusGraphQLApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -129,9 +129,9 @@ class ZaiusGraphQLApiManager { // 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, _, error in - guard error != nil, let data = data else { - let msg = error?.localizedDescription ?? "invalid data" + let task = session.dataTask(with: urlRequest) { data, response, error in + 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)" } @@ -139,6 +139,12 @@ class ZaiusGraphQLApiManager { return } + let status = response.statusCode + guard status < 400 else { + completionHandler(nil, .fetchSegmentsFailed("\(status)")) + return + } + guard let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { completionHandler(nil, .fetchSegmentsFailed("decode error")) return diff --git a/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift index e44a92e2..4447c09f 100644 --- a/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift @@ -18,26 +18,28 @@ import XCTest class ODPSegmentManagerTests: XCTestCase { var manager: ODPSegmentManager! - var odpConfig = OptimizelyODPConfig() + var odpConfig: OptimizelyODPConfig! var options = [OptimizelySegmentOption]() - var apiKey = "valid" - var apiHost = "host" - var userKey = "vuid" var userValue = "test-user" + static var receivedApiKey: String? + static var receivedApiHost: String? override func setUp() { + odpConfig = OptimizelyODPConfig() + odpConfig.apiKey = "valid" + odpConfig.apiHost = "host" + manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: MockZaiusApiManager()) } - func testSuccess_cacheMiss() { + func testFetchSegmentsSuccess_cacheMiss() { setCache(userKey, "123", ["a"]) let sem = DispatchSemaphore(value: 0) - manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: [], @@ -46,15 +48,13 @@ class ODPSegmentManagerTests: XCTestCase { XCTAssertEqual(["new-customer"], segments) sem.signal() } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } - func testSuccess_cacheHit() { + func testFetchSegmentsSuccess_cacheHit() { setCache(userKey, userValue, ["a"]) let sem = DispatchSemaphore(value: 0) - manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: [], @@ -63,25 +63,57 @@ class ODPSegmentManagerTests: XCTestCase { XCTAssertEqual(["a"], segments) sem.signal() } - - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - func testError() { - let sem = DispatchSemaphore(value: 0) - + func testFetchSegmentsError() { odpConfig.apiKey = "invalid-key" + let sem = DispatchSemaphore(value: 0) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: [], options: []) { segments, error in XCTAssertNotNil(error) - XCTAssert(segments!.isEmpty ) + XCTAssertNil(segments) sem.signal() } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + // MARK: - OdpConfig + + func testOdpConfig() { + // default - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + odpConfig = OptimizelyODPConfig() + manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: MockZaiusApiManager()) + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + segmentsToCheck: [], + options: options) { _, _ in } + + XCTAssertEqual(100, manager.segmentsCache.size) + XCTAssertEqual(600, manager.segmentsCache.timeoutInSecs) + XCTAssertNil(ODPSegmentManagerTests.receivedApiHost) + XCTAssertNil(ODPSegmentManagerTests.receivedApiKey) + + // custom configuration + + odpConfig = OptimizelyODPConfig(segmentsCacheSize: 3, + segmentsCacheTimeoutInSecs: 30, + apiHost: "test-host", + apiKey: "test-key") + manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: MockZaiusApiManager()) + manager.fetchQualifiedSegments(userKey: userKey, + userValue: userValue, + segmentsToCheck: [], + options: options) { _, _ in } + + XCTAssertEqual(3, manager.segmentsCache.size) + XCTAssertEqual(39, manager.segmentsCache.timeoutInSecs) + XCTAssertEqual("test-host", ODPSegmentManagerTests.receivedApiHost) + XCTAssertEqual("test-key", ODPSegmentManagerTests.receivedApiKey) } // MARK: - OptimizelySegmentOption @@ -91,7 +123,6 @@ class ODPSegmentManagerTests: XCTestCase { options = [.ignoreCache] let sem = DispatchSemaphore(value: 0) - manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: [], @@ -101,8 +132,7 @@ class ODPSegmentManagerTests: XCTestCase { XCTAssertEqual(1, self.cacheCount, "cache save should be skipped as well") sem.signal() } - - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } func testOptions_resetCache() { @@ -112,7 +142,6 @@ class ODPSegmentManagerTests: XCTestCase { options = [.resetCache] let sem = DispatchSemaphore(value: 0) - manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: [], @@ -123,8 +152,11 @@ class ODPSegmentManagerTests: XCTestCase { XCTAssertEqual(1, self.cacheCount, "cache should be reset and then add a new one") sem.signal() } - - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + } + + func testMakeCacheKey() { + XCTAssertEqual("vuid-$-test-user", manager.makeCacheKey(userKey, userValue)) } // MARK: - Utils @@ -153,9 +185,12 @@ class ODPSegmentManagerTests: XCTestCase { userValue: String, segmentsToCheck: [String], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + ODPSegmentManagerTests.receivedApiKey = apiKey + ODPSegmentManagerTests.receivedApiHost = apiHost + DispatchQueue.global().async { if apiKey == "invalid-key" { - completionHandler([], OptimizelyError.fetchSegmentsFailed("invalid key")) + completionHandler([], OptimizelyError.fetchSegmentsFailed("403")) } else { completionHandler(["new-customer"], nil) } diff --git a/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift b/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift index b8052dbd..4a4171b1 100644 --- a/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift @@ -163,6 +163,46 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } + func testFetchQualifiedSegments_400() { + let api = MockZaiusApiManager(MockZaiusUrlSession(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 = MockZaiusApiManager(MockZaiusUrlSession(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 testMakeSubsetFilter() { From 4030b7f688ee5e94e1e06dfd3e0f1353035f93d8 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 Jul 2022 17:56:34 -0700 Subject: [PATCH 51/84] add odp event tests --- Sources/ODP/ODPEvent.swift | 12 ++ Sources/ODP/ODPEventManager.swift | 30 ++- Sources/Utils/Utils.swift | 1 - .../ODPEventManagerTests.swift | 203 ++++++++++++++++++ .../ODPSegmentManagerTests.swift | 2 +- Tests/TestUtils/OTUtils.swift | 4 +- 6 files changed, 232 insertions(+), 20 deletions(-) diff --git a/Sources/ODP/ODPEvent.swift b/Sources/ODP/ODPEvent.swift index a5531452..42332fa9 100644 --- a/Sources/ODP/ODPEvent.swift +++ b/Sources/ODP/ODPEvent.swift @@ -65,4 +65,16 @@ struct ODPEvent: Codable { "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/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index f988a01d..09426b7e 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -43,24 +43,22 @@ class ODPEventManager { // MARK: - events func registerVUID(vuid: String) { - let event = ODPEvent(type: Constants.ODP.eventType, - action: "client_initialized", - identifiers: [ - Constants.ODP.keyForVuid: vuid - ], - data: addCommonEventData()) - dispatch(event) + sendEvent(type: Constants.ODP.eventType, + action: "client_initialized", + identifiers: [ + Constants.ODP.keyForVuid: vuid + ], + data: [:]) } func identifyUser(vuid: String, userId: String) { - let event = ODPEvent(type: Constants.ODP.eventType, - action: "identified", - identifiers: [ - Constants.ODP.keyForVuid: vuid, - Constants.ODP.keyForUserId: userId - ], - data: addCommonEventData()) - dispatch(event) + 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]) { @@ -80,7 +78,7 @@ class ODPEventManager { "data_source_version": Utils.sdkVersion, // "3.10.2" // [optional] client sdks only - "os": Utils.os, // ("iOS", "Android", "Mac OS", "Windows", "Linux", ...) + "os": "iOS", // ("iOS", "Android", "Mac OS", "Windows", "Linux", ...) "os_version": Utils.osVersion, // "13.2", ... "device_type": Utils.deviceType, // fixed set = ("Phone", "Tablet", "Smart TV", “PC”, "Other") "model": Utils.deviceModel // ("iPhone 12", "iPad 2", "Pixel 2", "SM-A515F", ...) diff --git a/Sources/Utils/Utils.swift b/Sources/Utils/Utils.swift index 8284ede5..d26bcace 100644 --- a/Sources/Utils/Utils.swift +++ b/Sources/Utils/Utils.swift @@ -23,7 +23,6 @@ class Utils { static var sdkVersion: String = OPTIMIZELYSDKVERSION static let swiftSdkClientName = "swift-sdk" - static let os = UIDevice.current.systemName static let osVersion = UIDevice.current.systemVersion static let deviceModel = UIDevice.current.model static var deviceType: String { diff --git a/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift b/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift index ffe3b922..d6327b3a 100644 --- a/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift @@ -17,5 +17,208 @@ import XCTest class ODPEventManagerTests: XCTestCase { + var manager: ODPEventManager! + var odpConfig: OptimizelyODPConfig! + + var options = [OptimizelySegmentOption]() + + var userKey = "vuid" + var userValue = "test-user" + static var receivedApiKey: String? + static var receivedApiHost: String? + + let customData: [String: Any] = ["key-1": "value-1", + "key-2": 12.5, + "model": "overruled"] + + override func setUp() { + OTUtils.createDocumentDirectoryIfNotAvailable() + + // no valid apiKey, so flush will return immediately + odpConfig = OptimizelyODPConfig() + + manager = ODPEventManager(sdkKey: "any", + odpConfig: odpConfig, + apiManager: MockZaiusApiManager()) + } + + override func tearDown() { + OTUtils.clearAllEventQueues() + } + + // 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(3) + 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()! + 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 = OptimizelyODPConfig() + odpConfig.apiKey = "valid" + odpConfig.apiHost = "host" + + manager = ODPEventManager(sdkKey: "any", + odpConfig: odpConfig, + apiManager: MockZaiusApiManager()) + 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_apiKey() { + let event = ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + + // apiKey is not ready + + manager.dispatch(event) + manager.dispatch(event) + manager.dispatch(event) + + XCTAssertEqual(3, manager.eventQueue.count) + sleep(1) + XCTAssertEqual(3, manager.eventQueue.count, "not flushed since apiKey is not ready") + + // apiKey is ready + + odpConfig.apiKey = "valid" + odpConfig.apiHost = "host" + + manager.flush() + + sleep(1) + XCTAssertEqual(0, manager.eventQueue.count) + } + + func testFlush_batch_1() { + XCTFail() + } + + func testFlush_batch_3() { + XCTFail() + } + + func testFlush_batch_moreThanBatchSize() { + XCTFail() + } + + func testFlush_emptyQueue() { + XCTFail() + } + + // MARK: - Errors + + func testFlushError_retry() { + XCTFail() + } + + func testFlushError_noRetry() { + XCTFail() + } + + // MARK: - OdpConfig + + func testOdpConfig() { + odpConfig.apiHost = "test-host" + odpConfig.apiKey = "test-key" + + manager = ODPEventManager(sdkKey: "any", + odpConfig: odpConfig, + apiManager: MockZaiusApiManager()) + + let event = ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + manager.dispatch(event) + manager.flush() + + XCTAssertEqual("test-host", ODPEventManagerTests.receivedApiHost) + XCTAssertEqual("test-key", ODPEventManagerTests.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"] as! String) == "iOS") + XCTAssert((data["os_version"] as! String).count > 3) + XCTAssert((data["device_type"] as! String).count > 3) + + // overruled prop + if customData.isEmpty { + XCTAssert((data["model"] as! String).count > 3) + } else { + XCTAssert((data["model"] as! String) == "overruled") + } + + // other custom data + XCTAssert((data["key-1"] as! String) == "value-1") + XCTAssert((data["key-2"] as! Double) == 12.5) + } + + // MARK: - MockZaiusApiManager + + class MockZaiusApiManager: ZaiusRestApiManager { + + override func sendODPEvents(apiKey: String, + apiHost: String, + events: [ODPEvent], + completionHandler: @escaping (OptimizelyError?) -> Void) { + ODPEventManagerTests.receivedApiKey = apiKey + ODPEventManagerTests.receivedApiHost = apiHost + + 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/ODPSegmentManagerTests.swift b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift index 4447c09f..99b6c2a8 100644 --- a/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift @@ -175,7 +175,7 @@ class ODPSegmentManagerTests: XCTestCase { return manager.segmentsCache.map.count } - // MARK: - MockSegmentManager + // MARK: - MockZaiusApiManager class MockZaiusApiManager: ZaiusGraphQLApiManager { diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index 4921929e..34cd2cd6 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -233,8 +233,8 @@ class OTUtils { } static func clearAllEventQueues() { - removeAllFiles(including: "OPTEventQueue", in: .documentDirectory) - removeAllFiles(including: "OPTEventQueue", in: .cachesDirectory) + removeAllFiles(including: "OPTEvent", in: .documentDirectory) + removeAllFiles(including: "OPTEvent", in: .cachesDirectory) } // MARK: - datafiles From 09647bb0c4e12182747cafbf87f11a296df423a0 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 5 Jul 2022 17:47:30 -0700 Subject: [PATCH 52/84] add more tests for ODP --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 68 ++++---- Sources/ODP/ODPEventManager.swift | 18 ++- Sources/ODP/ODPManager.swift | 23 ++- Sources/ODP/ODPSegmentManager.swift | 6 +- ...VUIDManager.swift => ODPVUIDManager.swift} | 6 +- Sources/ODP/OptimizelyODPConfig.swift | 40 +++-- Sources/ODP/OptimizelySegmentOption.swift | 2 - Sources/Optimizely/OptimizelyClient.swift | 5 +- .../DecisionListenerTests.swift | 6 +- .../LRUCacheTests.swift | 13 +- .../ODPEventManagerTests.swift | 122 ++++++++++---- .../ODPManagerTests.swift | 150 ++++++++++++++++++ .../ODPSegmentManagerTests.swift | 33 ++-- .../ODPVUIDManagerTests.swift | 34 ++++ .../ODPZaiusRestApiManagerTests.swift | 17 +- .../OptimizelyUserContextTests_Segments.swift | 33 ---- 16 files changed, 411 insertions(+), 165 deletions(-) rename Sources/ODP/{VUIDManager.swift => ODPVUIDManager.swift} (95%) diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index f74a3c4e..a873b2a0 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1867,22 +1867,22 @@ 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 /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9432852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9442852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9452852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9462852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9472852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9482852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9492852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E94A2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E94B2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E94C2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E94D2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E94E2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E94F2852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9502852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.swift */; }; - 84E2E9512852A378001114AB /* VUIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E9412852A378001114AB /* VUIDManager.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 /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; 84E2E96228540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; 84E2E96328540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; @@ -2366,7 +2366,7 @@ 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPZaiusRestApiManagerTests.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 /* VUIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VUIDManager.swift; sourceTree = ""; }; + 84E2E9412852A378001114AB /* ODPVUIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ODPVUIDManager.swift; sourceTree = ""; }; 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyODPConfig.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 = ""; }; @@ -2585,7 +2585,7 @@ isa = PBXGroup; children = ( 6E6522DD278E4F3800954EA1 /* ODPManager.swift */, - 84E2E9412852A378001114AB /* VUIDManager.swift */, + 84E2E9412852A378001114AB /* ODPVUIDManager.swift */, 848617C72863DC2700B7F41B /* ODPSegmentManager.swift */, 84E2E9712855875E001114AB /* ODPEventManager.swift */, 848617FA286CF33700B7F41B /* ODPEvent.swift */, @@ -4090,7 +4090,7 @@ 6E14CDA92423F9C300010234 /* Utils.swift in Sources */, 6EF8DE1F24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E14CD882423F9A100010234 /* AttributeValue.swift in Sources */, - 84E2E9492852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9492852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E14CD822423F9A100010234 /* DataStoreFile.swift in Sources */, 6E14CDA42423F9C300010234 /* Notifications.swift in Sources */, 6E20050B26B4D28500278087 /* MockLogger.swift in Sources */, @@ -4163,7 +4163,7 @@ 6E424D0126324B620081004A /* SemanticVersion.swift in Sources */, 6E424D0226324B620081004A /* Audience.swift in Sources */, 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, - 84E2E9482852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9482852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */, 6E424D0526324B620081004A /* ConditionHolder.swift in Sources */, 6E424D5226324C4D0081004A /* OptimizelyClient+Decide.swift in Sources */, @@ -4269,7 +4269,7 @@ 6E75190122C520D500B2B157 /* Attribute.swift in Sources */, 848617DB2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E7516B322C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, - 84E2E9432852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9432852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E7517D522C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75187122C520D400B2B157 /* Variation.swift in Sources */, @@ -4391,7 +4391,7 @@ 6E7518F022C520D500B2B157 /* ConditionHolder.swift in Sources */, 6EF8DE2424BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75183022C520D400B2B157 /* BatchEvent.swift in Sources */, - 84E2E94E2852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E94E2852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E75192022C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E9B117922C5487A00C22D81 /* tvOSOnlyTests.swift in Sources */, 6E20051026B4D28500278087 /* MockLogger.swift in Sources */, @@ -4463,7 +4463,7 @@ 6ECB60D7234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m in Sources */, 6E75195822C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E424C03263228FD0081004A /* AtomicDictionary.swift in Sources */, - 84E2E94A2852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E94A2852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E994B3A25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75170A22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E9B11AC22C5489300C22D81 /* OTUtils.swift in Sources */, @@ -4582,7 +4582,7 @@ 6E7518BF22C520D400B2B157 /* Variable.swift in Sources */, 6E75181722C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75185322C520D400B2B157 /* ProjectConfig.swift in Sources */, - 84E2E94D2852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E94D2852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E75173D22C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E7516E922C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7518A722C520D400B2B157 /* FeatureFlag.swift in Sources */, @@ -4734,7 +4734,7 @@ 6E9B117822C5487100C22D81 /* DataStoreTests.swift in Sources */, 6E75171B22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75195122C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 84E2E94F2852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E94F2852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E75176322C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E9B117722C5487100C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */, 6E7517DD22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, @@ -4812,7 +4812,7 @@ 84861809286CF33700B7F41B /* ODPEvent.swift in Sources */, 6E75171022C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C822C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, - 84E2E9502852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9502852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E75194622C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185622C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20051226B4D28600278087 /* MockLogger.swift in Sources */, @@ -4977,7 +4977,7 @@ 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */, - 84E2E9472852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9472852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E623F06253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E424BE2263228E90081004A /* AtomicArray.swift in Sources */, 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, @@ -5076,7 +5076,7 @@ 84861804286CF33700B7F41B /* ODPEvent.swift in Sources */, 6E75170B22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C322C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, - 84E2E94B2852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E94B2852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E75194122C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185122C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20050D26B4D28500278087 /* MockLogger.swift in Sources */, @@ -5207,7 +5207,7 @@ 6E7517E622C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171822C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174822C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 84E2E94C2852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E94C2852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E7518FA22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7516E822C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75191222C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, @@ -5307,7 +5307,7 @@ 6E7517EB22C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171D22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174D22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 84E2E9512852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9512852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E7518FF22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7516ED22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75191722C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, @@ -5404,7 +5404,7 @@ 6E75195422C520D500B2B157 /* OPTBucketer.swift in Sources */, 848617DA2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E75171E22C520D400B2B157 /* OptimizelyResult.swift in Sources */, - 84E2E9422852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9422852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75189422C520D400B2B157 /* Experiment.swift in Sources */, @@ -5526,7 +5526,7 @@ 6E7518DE22C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6EF8DE1D24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7518EA22C520D400B2B157 /* ConditionHolder.swift in Sources */, - 84E2E9462852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9462852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E75182A22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75191A22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E20050826B4D28500278087 /* MockLogger.swift in Sources */, @@ -5611,7 +5611,7 @@ 75C71A2125E454460084187E /* EventForDispatch.swift in Sources */, 75C71A2225E454460084187E /* SemanticVersion.swift in Sources */, 75C71A2325E454460084187E /* Audience.swift in Sources */, - 84E2E9452852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9452852A378001114AB /* ODPVUIDManager.swift in Sources */, 6E6522E1278E4F3800954EA1 /* ODPManager.swift in Sources */, 75C71A2425E454460084187E /* AttributeValue.swift in Sources */, 75C71A2525E454460084187E /* ConditionLeaf.swift in Sources */, @@ -5690,7 +5690,7 @@ BD6485502491474500F30986 /* OPTBucketer.swift in Sources */, 848617DC2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, BD6485512491474500F30986 /* OptimizelyResult.swift in Sources */, - 84E2E9442852A378001114AB /* VUIDManager.swift in Sources */, + 84E2E9442852A378001114AB /* ODPVUIDManager.swift in Sources */, BD6485522491474500F30986 /* Constants.swift in Sources */, BD6485532491474500F30986 /* DefaultLogger.swift in Sources */, BD6485542491474500F30986 /* Experiment.swift in Sources */, diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/ODPEventManager.swift index 09426b7e..4004962b 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/ODPEventManager.swift @@ -25,7 +25,7 @@ class ODPEventManager { let maxFailureCount = 3 let queueLock: DispatchQueue let eventQueue: DataStoreQueueStackImpl - + let logger = OPTLoggerFactory.getLogger() init(sdkKey: String, odpConfig: OptimizelyODPConfig, apiManager: ZaiusRestApiManager? = nil) { @@ -94,6 +94,11 @@ class ODPEventManager { // MARK: - dispatch func dispatch(_ event: ODPEvent) { + guard odpConfig.enabled else { + logger.d("ODP has been disabled.") + return + } + guard eventQueue.count < maxQueueSize else { let error = OptimizelyError.eventDispatchFailed("ODP EventQueue is full") self.logger.e(error) @@ -105,8 +110,13 @@ class ODPEventManager { } func flush() { - guard let odpApiKey = odpConfig.apiKey else { - logger.d("ODP: event cannot be dispatched since apiKey not defined") + guard let odpApiKey = odpConfig.apiKey, let odpApiHost = odpConfig.apiHost else { + if !odpConfig.enabled { + queueLock.async { + // clean up pending events if ODP is disabled + _ = self.eventQueue.removeFirstItems(count: self.maxQueueSize) + } + } return } @@ -141,7 +151,7 @@ class ODPEventManager { notify.enter() self.zaiusMgr.sendODPEvents(apiKey: odpApiKey, - apiHost: self.odpConfig.apiHost, + apiHost: odpApiHost, events: events) { error in defer { notify.leave() // our send is done. diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift index 35a5e05b..d4288e4f 100644 --- a/Sources/ODP/ODPManager.swift +++ b/Sources/ODP/ODPManager.swift @@ -19,7 +19,7 @@ import Foundation class ODPManager { let odpConfig: OptimizelyODPConfig - let vuidManager: VUIDManager + let vuidManager: ODPVUIDManager let segmentManager: ODPSegmentManager let eventManager: ODPEventManager @@ -27,11 +27,11 @@ class ODPManager { init(sdkKey: String, odpConfig: OptimizelyODPConfig, - vuidManager: VUIDManager? = nil, + vuidManager: ODPVUIDManager? = nil, segmentManager: ODPSegmentManager? = nil, eventManager: ODPEventManager? = nil) { self.odpConfig = odpConfig - self.vuidManager = vuidManager ?? VUIDManager.shared + self.vuidManager = vuidManager ?? ODPVUIDManager.shared self.segmentManager = segmentManager ?? ODPSegmentManager(odpConfig: odpConfig) self.eventManager = eventManager ?? ODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) @@ -57,19 +57,16 @@ class ODPManager { } func sendEvent(type: String, action: String, identifiers: [String: String], data: [String: Any]) { - eventManager.sendEvent(type: type, action: action, identifiers: identifiers, data: data) + 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?) { - guard let apiKey = apiKey, let apiHost = apiHost else { - logger.w("ODP: invalid apiKey or apiHost") - return - } - - odpConfig.apiKey = apiKey - odpConfig.apiHost = apiHost - - // flush all ODP events waiting for apiKey + odpConfig.update(apiKey: apiKey, apiHost: apiHost) eventManager.flush() } diff --git a/Sources/ODP/ODPSegmentManager.swift b/Sources/ODP/ODPSegmentManager.swift index 08208878..ae23c897 100644 --- a/Sources/ODP/ODPSegmentManager.swift +++ b/Sources/ODP/ODPSegmentManager.swift @@ -36,8 +36,8 @@ class ODPSegmentManager { segmentsToCheck: [String], options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - guard let odpApiKey = odpConfig.apiKey else { - completionHandler(nil, .fetchSegmentsFailed("apiKey not defined")) + guard let odpApiKey = odpConfig.apiKey, let odpApiHost = odpConfig.apiHost else { + completionHandler(nil, .fetchSegmentsFailed("apiKey/apiHost not defined")) return } @@ -58,7 +58,7 @@ class ODPSegmentManager { } zaiusMgr.fetchSegments(apiKey: odpApiKey, - apiHost: odpConfig.apiHost, + apiHost: odpApiHost, userKey: userKey, userValue: userValue, segmentsToCheck: segmentsToCheck) { segments, err in diff --git a/Sources/ODP/VUIDManager.swift b/Sources/ODP/ODPVUIDManager.swift similarity index 95% rename from Sources/ODP/VUIDManager.swift rename to Sources/ODP/ODPVUIDManager.swift index 27a50106..942af62b 100644 --- a/Sources/ODP/VUIDManager.swift +++ b/Sources/ODP/ODPVUIDManager.swift @@ -16,12 +16,12 @@ import Foundation -class VUIDManager { +class ODPVUIDManager { var vuid: String = "" let logger = OPTLoggerFactory.getLogger() // a single vuid should be shared for all SDK instances - static let shared = VUIDManager() + static let shared = ODPVUIDManager() init() { self.vuid = load() @@ -38,7 +38,7 @@ class VUIDManager { // MARK: - VUID Store -extension VUIDManager { +extension ODPVUIDManager { // UserDefaults format: (keep the most recent vuid info only) // "optimizely-odp": { diff --git a/Sources/ODP/OptimizelyODPConfig.swift b/Sources/ODP/OptimizelyODPConfig.swift index 063576b4..89571f91 100644 --- a/Sources/ODP/OptimizelyODPConfig.swift +++ b/Sources/ODP/OptimizelyODPConfig.swift @@ -22,20 +22,27 @@ public class OptimizelyODPConfig { /// timeout in seconds (default = 600) of audience segments cache (optional) let segmentsCacheTimeoutInSecs: Int /// The host URL for the ODP audience segments API (optional). If not provided, SDK will use the default host in datafile. - private var _apiHost: String = "https://api.zaius.com" + private var _apiHost: String? /// The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile. private var _apiKey: String? + /// enabled by default (disabled when datafile has no ODP key/host settings) + private var _enabled = true let queue = DispatchQueue(label: "odpConfig") public init(segmentsCacheSize: Int = 100, - segmentsCacheTimeoutInSecs: Int = 600, - apiHost: String = "https://api.zaius.com", - apiKey: String? = nil) { + segmentsCacheTimeoutInSecs: Int = 600) { self.segmentsCacheSize = segmentsCacheSize self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs - self._apiHost = apiHost - self._apiKey = apiKey + } + + func update(apiKey: String?, apiHost: String?) { + self.apiKey = apiKey + self.apiHost = apiHost + + // disable future event queueing if datafile has no ODP settings + + self.enabled = (apiKey != nil) && (apiHost != nil) } } @@ -43,9 +50,9 @@ public class OptimizelyODPConfig { extension OptimizelyODPConfig { - var apiHost: String { + var apiHost: String? { get { - var value = "" + var value: String? queue.sync { value = _apiHost } @@ -72,6 +79,21 @@ extension OptimizelyODPConfig { } } } - + + var enabled: Bool { + get { + var value = false + queue.sync { + value = _enabled + } + return value + } + set { + queue.async { + self._enabled = newValue + } + } + } + } diff --git a/Sources/ODP/OptimizelySegmentOption.swift b/Sources/ODP/OptimizelySegmentOption.swift index b0bdf37e..dd186ca1 100644 --- a/Sources/ODP/OptimizelySegmentOption.swift +++ b/Sources/ODP/OptimizelySegmentOption.swift @@ -22,6 +22,4 @@ import Foundation case ignoreCache // reset cache case resetCache - // fetch all segments - case allSegments } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 2685de8f..f99477d4 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -938,8 +938,9 @@ extension OptimizelyClient { identifiers: identifiers, data: data) } - - var vuid: String { + + /// the device vuid (read only) + public var vuid: String { return odpManager.vuid } diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index c7ab7171..33d3ffa4 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift @@ -1214,8 +1214,7 @@ class FakeManager: OptimizelyClient { userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - segmentsCacheSize: Int? = nil, - segmentsCacheTimeout: Int? = nil) { + odpConfig: OptimizelyODPConfig? = nil) { // clear shared handlers HandlerRegistryService.shared.removeAll() @@ -1227,8 +1226,7 @@ class FakeManager: OptimizelyClient { userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, defaultDecideOptions: defaultDecideOptions, - segmentsCacheSize: segmentsCacheSize, - segmentsCacheTimeout: segmentsCacheTimeout) + odpConfig: odpConfig) let userProfileService = userProfileService ?? DefaultUserProfileService() self.decisionService = FakeDecisionService(userProfileService: userProfileService) diff --git a/Tests/OptimizelyTests-Common/LRUCacheTests.swift b/Tests/OptimizelyTests-Common/LRUCacheTests.swift index d08cfc10..0f8360e2 100644 --- a/Tests/OptimizelyTests-Common/LRUCacheTests.swift +++ b/Tests/OptimizelyTests-Common/LRUCacheTests.swift @@ -24,14 +24,8 @@ class LRUCacheTests: XCTestCase { XCTAssertEqual(2000, cache.timeoutInSecs) cache = LRUCache(size: 0, timeoutInSecs: 0) - - #if DEBUG - XCTAssertEqual(1, cache.size) - XCTAssertEqual(1, cache.timeoutInSecs) - #else XCTAssertEqual(10, cache.size) XCTAssertEqual(60, cache.timeoutInSecs) - #endif } } @@ -113,6 +107,13 @@ extension LRUCacheTests { XCTAssert(cache.map.isEmpty, "cache should be reset when detected that all items are stale") } + func testZeroSize() { + let cache = LRUCache(size: 0, timeoutInSecs: 1000) + + XCTAssertNil(cache.lookup(key: 1)) + cache.save(key: 1, value: 100) // [1] + XCTAssertNil(cache.lookup(key: 1)) + } } #endif diff --git a/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift b/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift index d6327b3a..c38778e0 100644 --- a/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift @@ -19,13 +19,12 @@ import XCTest class ODPEventManagerTests: XCTestCase { var manager: ODPEventManager! var odpConfig: OptimizelyODPConfig! - + var apiManager = MockZaiusApiManager() + var options = [OptimizelySegmentOption]() var userKey = "vuid" var userValue = "test-user" - static var receivedApiKey: String? - static var receivedApiHost: String? let customData: [String: Any] = ["key-1": "value-1", "key-2": 12.5, @@ -39,7 +38,7 @@ class ODPEventManagerTests: XCTestCase { manager = ODPEventManager(sdkKey: "any", odpConfig: odpConfig, - apiManager: MockZaiusApiManager()) + apiManager: apiManager) } override func tearDown() { @@ -89,12 +88,11 @@ class ODPEventManagerTests: XCTestCase { func testSendEvent_apiKey() { odpConfig = OptimizelyODPConfig() - odpConfig.apiKey = "valid" - odpConfig.apiHost = "host" + odpConfig.update(apiKey: "valid", apiHost: "host") manager = ODPEventManager(sdkKey: "any", odpConfig: odpConfig, - apiManager: MockZaiusApiManager()) + apiManager: apiManager) manager.sendEvent(type: "t1", action: "a1", identifiers: ["id-key-1": "id-value-1"], @@ -122,57 +120,116 @@ class ODPEventManagerTests: XCTestCase { // apiKey is ready - odpConfig.apiKey = "valid" - odpConfig.apiHost = "host" - + odpConfig.update(apiKey: "valid", apiHost: "host") + manager.flush() sleep(1) XCTAssertEqual(0, manager.eventQueue.count) } + // MARK: - batch + func testFlush_batch_1() { - XCTFail() + let events = [ + ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + ] + manager.dispatch(events[0]) + + odpConfig.update(apiKey: "valid", apiHost: "host") + sleep(1) + XCTAssertEqual(1, apiManager.receivedBatchEvents.count) + XCTAssertEqual(1, apiManager.receivedBatchEvents[0].count) + validateEvents(events, apiManager.receivedBatchEvents[0]) } func testFlush_batch_3() { - XCTFail() + 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") + sleep(1) + XCTAssertEqual(1, apiManager.receivedBatchEvents.count) + XCTAssertEqual(3, apiManager.receivedBatchEvents[0].count) + validateEvents(events, apiManager.receivedBatchEvents[0]) } func testFlush_batch_moreThanBatchSize() { - XCTFail() + 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") + sleep(1) + XCTAssertEqual(2, apiManager.receivedBatchEvents.count) + XCTAssertEqual(10, apiManager.receivedBatchEvents[0].count) + XCTAssertEqual(1, apiManager.receivedBatchEvents[1].count) + validateEvents(events, apiManager.receivedBatchEvents[0] + apiManager.receivedBatchEvents[1]) } func testFlush_emptyQueue() { - XCTFail() + odpConfig.update(apiKey: "valid", apiHost: "host") + sleep(1) + XCTAssertEqual(0, apiManager.receivedBatchEvents.count) } - // MARK: - Errors + // MARK: - errors func testFlushError_retry() { - XCTFail() + 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") + sleep(1) + XCTAssertEqual(3, apiManager.receivedBatchEvents.count, "should be retried 3 times (a batch of 2 events)") + XCTAssertEqual(2, manager.eventQueue.count, "the events should remain after giving up") } func testFlushError_noRetry() { - XCTFail() + 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") + sleep(1) + XCTAssertEqual(2, apiManager.receivedBatchEvents.count, "should not be retried (only once for each of batch events)") + XCTAssertEqual(10, apiManager.receivedBatchEvents[0].count) + XCTAssertEqual(5, apiManager.receivedBatchEvents[1].count) + XCTAssertEqual(0, manager.eventQueue.count, "all the events should be discarded") } // MARK: - OdpConfig func testOdpConfig() { - odpConfig.apiHost = "test-host" - odpConfig.apiKey = "test-key" - + odpConfig.update(apiKey: "test-key", apiHost: "test-host") + manager = ODPEventManager(sdkKey: "any", odpConfig: odpConfig, - apiManager: MockZaiusApiManager()) + apiManager: apiManager) let event = ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) manager.dispatch(event) manager.flush() - XCTAssertEqual("test-host", ODPEventManagerTests.receivedApiHost) - XCTAssertEqual("test-key", ODPEventManagerTests.receivedApiKey) + XCTAssertEqual("test-host", apiManager.receivedApiHost) + XCTAssertEqual("test-key", apiManager.receivedApiKey) } // MARK: - Utils @@ -198,17 +255,28 @@ class ODPEventManagerTests: XCTestCase { XCTAssert((data["key-2"] as! Double) == 12.5) } + func validateEvents(_ lhs: [ODPEvent], _ rhs: [ODPEvent]) { + XCTAssertEqual(lhs.count, rhs.count) + for i in 0.. Void) { - ODPEventManagerTests.receivedApiKey = apiKey - ODPEventManagerTests.receivedApiHost = apiHost - + receivedApiKey = apiKey + receivedApiHost = apiHost + receivedBatchEvents.append(events) + DispatchQueue.global().async { if apiKey == "invalid-key-no-retry" { completionHandler(OptimizelyError.odpEventFailed("403", false)) diff --git a/Tests/OptimizelyTests-Common/ODPManagerTests.swift b/Tests/OptimizelyTests-Common/ODPManagerTests.swift index c32c0963..92492922 100644 --- a/Tests/OptimizelyTests-Common/ODPManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPManagerTests.swift @@ -17,5 +17,155 @@ import XCTest class ODPManagerTests: XCTestCase { + var manager: ODPManager! + let sdkKey = "any" + let odpConfig = OptimizelyODPConfig() + func testFetchQualifiedSegments() { + let segmentManager = MockODPSegmentManager(odpConfig: odpConfig) + manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, segmentManager: segmentManager) + + let vuid = "VUID_123" + manager.fetchQualifiedSegments(userId: vuid, segmentsToCheck: ["seg-1"], options: [.ignoreCache]) { _, _ in } + + XCTAssertEqual(segmentManager.receivedUserKey, "vuid") + XCTAssertEqual(segmentManager.receivedUserValue, vuid) + XCTAssertEqual(segmentManager.receivedSegmentsToCheck, ["seg-1"]) + XCTAssertEqual(segmentManager.receivedOptions, [.ignoreCache]) + + let userId = "user-1" + manager.fetchQualifiedSegments(userId: userId, segmentsToCheck: [], options: []) { _, _ in } + + XCTAssertEqual(segmentManager.receivedUserKey, "fs_user_id") + XCTAssertEqual(segmentManager.receivedUserValue, "user-1") + XCTAssertEqual(segmentManager.receivedSegmentsToCheck, []) + XCTAssertEqual(segmentManager.receivedOptions, []) + } + + func testRegisterVUIDCalled() { + let eventManager = MockODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) + manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, eventManager: eventManager) + + // registerVUID is implicitly called on ODPManager init + + XCTAssertEqual(eventManager.receivedVuid, manager.vuid) + } + + func testIdentifyUser() { + let eventManager = MockODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) + manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, eventManager: eventManager) + + manager.identifyUser(userId: "user-1") + + XCTAssertEqual(eventManager.receivedVuid, manager.vuid) + XCTAssertEqual(eventManager.receivedUserId, "user-1") + } + + func testSendEvent() { + let eventManager = MockODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) + manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, eventManager: eventManager) + + // vuid is implicitly added to identifers + + 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 + + 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 testUpdateODPConfig() { + let eventManager = MockODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) + manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, eventManager: eventManager) + + // flush on valid apiKey updated (apiKey/apiHost propagated into submanagers) + + manager.updateODPConfig(apiKey: "key-1", apiHost: "host-1") + XCTAssertTrue(eventManager.flushCalled) + + XCTAssertEqual(manager.segmentManager.odpConfig.apiKey, "key-1") + XCTAssertEqual(manager.segmentManager.odpConfig.apiHost, "host-1") + XCTAssertTrue(manager.segmentManager.odpConfig.enabled) + XCTAssertEqual(manager.eventManager.odpConfig.apiKey, "key-1") + XCTAssertEqual(manager.eventManager.odpConfig.apiHost, "host-1") + XCTAssertTrue(manager.eventManager.odpConfig.enabled) + + // odp disabled with invalid apiKey (apiKey/apiHost propagated into submanagers) + + manager.updateODPConfig(apiKey: nil, apiHost: nil) + XCTAssertTrue(eventManager.flushCalled) + + XCTAssertEqual(manager.segmentManager.odpConfig.apiKey, nil) + XCTAssertEqual(manager.segmentManager.odpConfig.apiHost, nil) + XCTAssertFalse(manager.segmentManager.odpConfig.enabled) + XCTAssertEqual(manager.eventManager.odpConfig.apiKey, nil) + XCTAssertEqual(manager.eventManager.odpConfig.apiHost, nil) + XCTAssertFalse(manager.eventManager.odpConfig.enabled) + } + + func testVuid() { + manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig) + 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 flushCalled = 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.flushCalled = true + } + } + + class MockODPSegmentManager: ODPSegmentManager { + var receivedUserKey: String! + var receivedUserValue: String! + var receivedSegmentsToCheck: [String]! + var receivedOptions: [OptimizelySegmentOption]! + + override func fetchQualifiedSegments(userKey: String, + userValue: String, + segmentsToCheck: [String], + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + self.receivedUserKey = userKey + self.receivedUserValue = userValue + self.receivedSegmentsToCheck = segmentsToCheck + self.receivedOptions = options + } + } + } diff --git a/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift index 99b6c2a8..fddebb9a 100644 --- a/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift @@ -19,21 +19,19 @@ import XCTest class ODPSegmentManagerTests: XCTestCase { var manager: ODPSegmentManager! var odpConfig: OptimizelyODPConfig! + var apiManager = MockZaiusApiManager() var options = [OptimizelySegmentOption]() var userKey = "vuid" var userValue = "test-user" - static var receivedApiKey: String? - static var receivedApiHost: String? override func setUp() { odpConfig = OptimizelyODPConfig() - odpConfig.apiKey = "valid" - odpConfig.apiHost = "host" + odpConfig.update(apiKey: "valid", apiHost: "host") manager = ODPSegmentManager(odpConfig: odpConfig, - apiManager: MockZaiusApiManager()) + apiManager: apiManager) } func testFetchSegmentsSuccess_cacheMiss() { @@ -86,8 +84,7 @@ class ODPSegmentManagerTests: XCTestCase { func testOdpConfig() { // default - odpConfig = OptimizelyODPConfig() - manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: MockZaiusApiManager()) + manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: apiManager) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: [], @@ -95,16 +92,14 @@ class ODPSegmentManagerTests: XCTestCase { XCTAssertEqual(100, manager.segmentsCache.size) XCTAssertEqual(600, manager.segmentsCache.timeoutInSecs) - XCTAssertNil(ODPSegmentManagerTests.receivedApiHost) - XCTAssertNil(ODPSegmentManagerTests.receivedApiKey) // custom configuration + odpConfig.update(apiKey: "test-key", apiHost: "test-host") + odpConfig = OptimizelyODPConfig(segmentsCacheSize: 3, - segmentsCacheTimeoutInSecs: 30, - apiHost: "test-host", - apiKey: "test-key") - manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: MockZaiusApiManager()) + segmentsCacheTimeoutInSecs: 30) + manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: apiManager) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, segmentsToCheck: [], @@ -112,8 +107,8 @@ class ODPSegmentManagerTests: XCTestCase { XCTAssertEqual(3, manager.segmentsCache.size) XCTAssertEqual(39, manager.segmentsCache.timeoutInSecs) - XCTAssertEqual("test-host", ODPSegmentManagerTests.receivedApiHost) - XCTAssertEqual("test-key", ODPSegmentManagerTests.receivedApiKey) + XCTAssertEqual("test-key", apiManager.receivedApiKey) + XCTAssertEqual("test-host", apiManager.receivedApiHost) } // MARK: - OptimizelySegmentOption @@ -178,15 +173,17 @@ class ODPSegmentManagerTests: XCTestCase { // MARK: - MockZaiusApiManager class MockZaiusApiManager: ZaiusGraphQLApiManager { - + var receivedApiKey: String! + var receivedApiHost: String! + override func fetchSegments(apiKey: String, apiHost: String, userKey: String, userValue: String, segmentsToCheck: [String], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - ODPSegmentManagerTests.receivedApiKey = apiKey - ODPSegmentManagerTests.receivedApiHost = apiHost + receivedApiKey = apiKey + receivedApiHost = apiHost DispatchQueue.global().async { if apiKey == "invalid-key" { diff --git a/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift b/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift index af9d1d32..e59274a4 100644 --- a/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift @@ -17,5 +17,39 @@ import XCTest class ODPVUIDManagerTests: XCTestCase { + var manager = ODPVUIDManager() + + func testMakeVuid() { + let vuid = manager.makeVuid() + XCTAssertTrue(vuid.starts(with: "VUID_")) + XCTAssertTrue(vuid.count > 20) + } + + 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-odp") + + 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-odp") + + manager = ODPVUIDManager() + let vuid3 = manager.vuid + + XCTAssertTrue(vuid1 != vuid3) + } } diff --git a/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift b/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift index c8050af7..bcd373c7 100644 --- a/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift @@ -23,17 +23,16 @@ class ZaiusRestApiManagerTests: XCTestCase { let apiHost = "test-host" let events: [ODPEvent] = [ - ODPEvent(type: "fullstack", action: "a", identifiers: ["id-key-1": "id-value-1"], data: ["key-1": "value-1"]) + ODPEvent(type: "t1", action: "a1", identifiers: ["id-key-1": "id-value-1"], data: ["key-1": "value-1"]), + ODPEvent(type: "t2", action: "a2", identifiers: ["id-key-2": "id-value-2"], data: ["key-2": "value-2"]) ] - static var createdApiRequest: URLRequest? - func testSendODPEvents_validRequest() { - let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, - responseData: MockZaiusUrlSession.successResponseData)) + let session = MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.successResponseData) + let api = MockZaiusApiManager(session) api.sendODPEvents(apiKey: apiKey, apiHost: apiHost, events: events) { _ in } - let request = ZaiusRestApiManagerTests.createdApiRequest! + let request = session.receivedApiRequest! XCTAssertEqual(apiHost + "/v3/events", request.url?.absoluteString) XCTAssertEqual("POST", request.httpMethod) @@ -116,6 +115,9 @@ class ZaiusRestApiManagerTests: XCTestCase { override func getSession() -> URLSession { return mockUrlSession } + + override func sendODPEvents(apiKey: String, apiHost: String, events: [ODPEvent], completionHandler: @escaping (OptimizelyError?) -> Void) { + } } // MARK: - MockZaiusUrlSession @@ -125,6 +127,7 @@ class ZaiusRestApiManagerTests: XCTestCase { var statusCode: Int var withError: Bool var responseData: String? + var receivedApiRequest: URLRequest? class MockDataTask: URLSessionDataTask { var task: () -> Void @@ -146,7 +149,7 @@ class ZaiusRestApiManagerTests: XCTestCase { } override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - ZaiusRestApiManagerTests.createdApiRequest = request + receivedApiRequest = request return MockDataTask() { let statusCode = self.statusCode != 0 ? self.statusCode : 200 diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift index 61c97893..18fef174 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift @@ -275,25 +275,6 @@ extension OptimizelyUserContextTests_Segments { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } - func testLiveODPGraphQL_allSegments() { - let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - try! optimizely.start(datafile: datafile) - let user = optimizely.createUserContext(userId: testODPUserId) - - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: testODPApiKeyForAudienceSegments, - apiHost: testODPApiHost, - userKey: testODPUserKey, - userValue: testODPUserValue, - options: [.allSegments]) { segments, error in - XCTAssertNil(error) - XCTAssert(segments!.contains("has_email"), "segmentsToCheck are not passed to ODP, so fetching all segments.") - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) - } - - func testLiveODPGraphQL_defaultParameters() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) @@ -308,20 +289,6 @@ extension OptimizelyUserContextTests_Segments { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } - func testLiveODPGraphQL_defaultParameters_allSegments() { - let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - try! optimizely.start(datafile: datafile) - let user = optimizely.createUserContext(userId: testODPUserId) - - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(options: [.allSegments]) { segments, error in - XCTAssertNil(error) - XCTAssert(segments!.contains("has_email"), "segmentsToCheck are not passed to ODP, so fetching all segments.") - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) - } - func testLiveODPGraphQL_noDatafile() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) let user = optimizely.createUserContext(userId: testODPUserId) From d12c1225e055c43da3690eba847161b896a6170a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 7 Jul 2022 17:33:47 -0700 Subject: [PATCH 53/84] fix odp tests --- DemoSwiftApp/AppDelegate.swift | 13 +- .../xcschemes/DemoSwiftwatchOS.xcscheme | 25 +- DemoSwiftApp/Samples/SamplesForAPI.swift | 5 +- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 646 +++++++++--------- .../OptimizelyClient+Extension.swift | 6 +- .../ODP/{LRUCache.swift => LruCache.swift} | 2 +- Sources/ODP/ODPManager.swift | 77 --- ...imizelyODPConfig.swift => OdpConfig.swift} | 33 +- .../ODP/{ODPEvent.swift => OdpEvent.swift} | 6 +- ...entManager.swift => OdpEventManager.swift} | 39 +- Sources/ODP/OdpManager.swift | 105 +++ ...tManager.swift => OdpSegmentManager.swift} | 17 +- ...VUIDManager.swift => OdpVuidManager.swift} | 10 +- Sources/ODP/OptimizelySdkSettings.swift | 34 + Sources/ODP/ZaiusGraphQLApiManager.swift | 4 +- Sources/ODP/ZaiusRestApiManager.swift | 4 +- .../OptimizelyUserContext.swift | 2 +- Sources/Optimizely/OptimizelyClient.swift | 27 +- Sources/Optimizely/OptimizelyError.swift | 2 + .../OptimizelyClientTests_ODP.swift | 45 ++ .../DecisionListenerTests.swift | 4 +- ...RUCacheTests.swift => LruCacheTests.swift} | 48 +- ...Tests.swift => OdpEventManagerTests.swift} | 122 +++- ...nagerTests.swift => OdpManagerTests.swift} | 142 ++-- ...sts.swift => OdpSegmentManagerTests.swift} | 62 +- ...rTests.swift => OdpVuidManagerTests.swift} | 16 +- ...t => OdpZaiusGraphQLApiManagerTests.swift} | 0 ...wift => OdpZaiusRestApiManagerTests.swift} | 28 +- ...t => OptimizelyUserContextTests_ODP.swift} | 240 +++---- ...timizelyUserContextTests_ODP_Decide.swift} | 2 +- Tests/TestUtils/OTUtils.swift | 3 +- 31 files changed, 973 insertions(+), 796 deletions(-) rename Sources/ODP/{LRUCache.swift => LruCache.swift} (99%) delete mode 100644 Sources/ODP/ODPManager.swift rename Sources/ODP/{OptimizelyODPConfig.swift => OdpConfig.swift} (72%) rename Sources/ODP/{ODPEvent.swift => OdpEvent.swift} (95%) rename Sources/ODP/{ODPEventManager.swift => OdpEventManager.swift} (87%) create mode 100644 Sources/ODP/OdpManager.swift rename Sources/ODP/{ODPSegmentManager.swift => OdpSegmentManager.swift} (87%) rename Sources/ODP/{ODPVUIDManager.swift => OdpVuidManager.swift} (88%) create mode 100644 Sources/ODP/OptimizelySdkSettings.swift create mode 100644 Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift rename Tests/OptimizelyTests-Common/{LRUCacheTests.swift => LruCacheTests.swift} (85%) rename Tests/OptimizelyTests-Common/{ODPEventManagerTests.swift => OdpEventManagerTests.swift} (71%) rename Tests/OptimizelyTests-Common/{ODPManagerTests.swift => OdpManagerTests.swift} (51%) rename Tests/OptimizelyTests-Common/{ODPSegmentManagerTests.swift => OdpSegmentManagerTests.swift} (77%) rename Tests/OptimizelyTests-Common/{ODPVUIDManagerTests.swift => OdpVuidManagerTests.swift} (79%) rename Tests/OptimizelyTests-Common/{ODPZaiusGraphQLApiManagerTests.swift => OdpZaiusGraphQLApiManagerTests.swift} (100%) rename Tests/OptimizelyTests-Common/{ODPZaiusRestApiManagerTests.swift => OdpZaiusRestApiManagerTests.swift} (88%) rename Tests/OptimizelyTests-Common/{OptimizelyUserContextTests_Segments.swift => OptimizelyUserContextTests_ODP.swift} (50%) rename Tests/OptimizelyTests-Common/{OptimizelyUserContextTests_Segments_Decide.swift => OptimizelyUserContextTests_ODP_Decide.swift} (98%) diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 6127db65..75af7eae 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -130,16 +130,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @unknown default: print("Optimizely SDK initiliazation failed with unknown result") } - - - - - //self.startWithRootViewController() + + 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) +// SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely) +// SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely) +// SamplesForAPI.checkAudienceSegments(optimizely: self.optimizely) } } diff --git a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme index 7aa2b41d..1ebde8af 100644 --- a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme +++ b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme @@ -54,10 +54,8 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - - - - - + diff --git a/DemoSwiftApp/Samples/SamplesForAPI.swift b/DemoSwiftApp/Samples/SamplesForAPI.swift index e283e470..3db23843 100644 --- a/DemoSwiftApp/Samples/SamplesForAPI.swift +++ b/DemoSwiftApp/Samples/SamplesForAPI.swift @@ -266,10 +266,7 @@ class SamplesForAPI { static func checkAudienceSegments(optimizely: OptimizelyClient) { // override the default handler if cache size and timeout need to be customized let optimizely = OptimizelyClient(sdkKey: "FCnSegiEkRry9rhVMroit4", - periodicDownloadInterval: 60, - odpConfig: OptimizelyODPConfig(segmentsCacheSize: 12, - segmentsCacheTimeoutInSecs: 123, - apiKey: "sample-api-key")) + periodicDownloadInterval: 60) optimizely.start { result in if case .failure(let error) = result { print("[AudienceSegments] SDK initialization failed: \(error)") diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index a873b2a0..1b628195 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -625,38 +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 */; }; + 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 */; }; @@ -1753,8 +1753,25 @@ 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 */; }; + 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 */; }; @@ -1773,22 +1790,22 @@ 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 */; }; - 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 */; }; + 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 /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; 848617DB2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; 848617DC2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */; }; @@ -1821,34 +1838,34 @@ 848617F72863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; 848617F82863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */; }; 848617F92863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848617D92863E21200B7F41B /* ZaiusRestApiManager.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 /* ODPZaiusGraphQLApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861819286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift */; }; - 8486181C286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861819286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift */; }; - 8486181D286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift */; }; - 8486181E286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.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 /* OdpZaiusGraphQLApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861819286D188B00B7F41B /* OdpZaiusGraphQLApiManagerTests.swift */; }; + 8486181C286D188B00B7F41B /* OdpZaiusGraphQLApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84861819286D188B00B7F41B /* OdpZaiusGraphQLApiManagerTests.swift */; }; + 8486181D286D188B00B7F41B /* OdpZaiusRestApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486181A286D188B00B7F41B /* OdpZaiusRestApiManagerTests.swift */; }; + 8486181E286D188B00B7F41B /* OdpZaiusRestApiManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486181A286D188B00B7F41B /* OdpZaiusRestApiManagerTests.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 */; }; @@ -1867,54 +1884,54 @@ 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 /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96228540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96328540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96428540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96528540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96628540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96728540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96828540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96928540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96A28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96B28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96C28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96D28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96E28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E96F28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */; }; - 84E2E97028540B5E001114AB /* OptimizelyODPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E2E96028540B5E001114AB /* OptimizelyODPConfig.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 */; }; + 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 */; }; @@ -1931,16 +1948,16 @@ 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_Segments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */; }; - 84F6BAB427FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.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 */; }; 84F6BACC27FCFAD7004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; 84F6BAD727FCFB14004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; 84F6BAD827FCFB15004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; 84F6BAD927FCFB17004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; 84F6BADA27FCFB18004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; 84F6BADB27FCFB21004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; - 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift */; }; - 84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.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 */; }; @@ -2163,8 +2180,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 = ""; }; + 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 = ""; }; @@ -2351,28 +2368,30 @@ 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 = ""; }; + 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 = ""; }; - 848617C72863DC2700B7F41B /* ODPSegmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPSegmentManager.swift; sourceTree = ""; }; + 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpSegmentManager.swift; sourceTree = ""; }; 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusGraphQLApiManager.swift; sourceTree = ""; }; 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusRestApiManager.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 /* ODPZaiusGraphQLApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPZaiusGraphQLApiManagerTests.swift; sourceTree = ""; }; - 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ODPZaiusRestApiManagerTests.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 /* OdpZaiusGraphQLApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpZaiusGraphQLApiManagerTests.swift; sourceTree = ""; }; + 8486181A286D188B00B7F41B /* OdpZaiusRestApiManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpZaiusRestApiManagerTests.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 /* OptimizelyODPConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyODPConfig.swift; sourceTree = ""; }; - 84E2E9712855875E001114AB /* ODPEventManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ODPEventManager.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_Segments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments.swift; sourceTree = ""; }; + 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP.swift; sourceTree = ""; }; 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_audience_segments.json; sourceTree = ""; }; - 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Segments_Decide.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 = ""; }; @@ -2584,14 +2603,14 @@ 6E6522B8278DF20F00954EA1 /* ODP */ = { isa = PBXGroup; children = ( - 6E6522DD278E4F3800954EA1 /* ODPManager.swift */, - 84E2E9412852A378001114AB /* ODPVUIDManager.swift */, - 848617C72863DC2700B7F41B /* ODPSegmentManager.swift */, - 84E2E9712855875E001114AB /* ODPEventManager.swift */, - 848617FA286CF33700B7F41B /* ODPEvent.swift */, + 6E6522DD278E4F3800954EA1 /* OdpManager.swift */, + 84E2E9412852A378001114AB /* OdpVuidManager.swift */, + 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */, + 84E2E9712855875E001114AB /* OdpEventManager.swift */, + 848617FA286CF33700B7F41B /* OdpEvent.swift */, 84B4D74F27E2A7550078CDA4 /* OptimizelySegmentOption.swift */, - 84E2E96028540B5E001114AB /* OptimizelyODPConfig.swift */, - 6E6522FF278E688B00954EA1 /* LRUCache.swift */, + 84518B20287737070023F104 /* OdpConfig.swift */, + 6E6522FF278E688B00954EA1 /* LruCache.swift */, 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */, 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */, ); @@ -2652,6 +2671,7 @@ isa = PBXGroup; children = ( 6E75166722C520D400B2B157 /* OptimizelyError.swift */, + 84E2E96028540B5E001114AB /* OptimizelySdkSettings.swift */, 6E75166822C520D400B2B157 /* OptimizelyLogLevel.swift */, 6E75166922C520D400B2B157 /* OptimizelyClient.swift */, 6E75166A22C520D400B2B157 /* OptimizelyClient+ObjC.swift */, @@ -2864,7 +2884,6 @@ 6E75197E22C5211100B2B157 /* OptimizelyTests-Common */ = { isa = PBXGroup; children = ( - 84958C5D280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift */, 6E75198522C5211100B2B157 /* BatchEventBuilderTest.swift */, 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */, 6E75198722C5211100B2B157 /* BatchEventBuilderTests_Events.swift */, @@ -2887,25 +2906,26 @@ 6E75198922C5211100B2B157 /* DefaultUserProfileServiceTests.swift */, 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */, 6E75198E22C5211100B2B157 /* LoggerTests.swift */, - 8428D3CF2807337400D0FB0C /* LRUCacheTests.swift */, + 8428D3CF2807337400D0FB0C /* LruCacheTests.swift */, 6E75197F22C5211100B2B157 /* MurmurTests.swift */, 6E0207A7272A11CF008C3711 /* NetworkReachabilityTests.swift */, 6E75198B22C5211100B2B157 /* NotificationCenterTests.swift */, - 84861810286D0B8900B7F41B /* ODPEventManagerTests.swift */, - 8486180E286D0B8900B7F41B /* ODPManagerTests.swift */, - 8486180D286D0B8900B7F41B /* ODPSegmentManagerTests.swift */, - 8486180F286D0B8900B7F41B /* ODPVUIDManagerTests.swift */, - 84861819286D188B00B7F41B /* ODPZaiusGraphQLApiManagerTests.swift */, - 8486181A286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift */, + 84861810286D0B8900B7F41B /* OdpEventManagerTests.swift */, + 8486180E286D0B8900B7F41B /* OdpManagerTests.swift */, + 8486180D286D0B8900B7F41B /* OdpSegmentManagerTests.swift */, + 8486180F286D0B8900B7F41B /* OdpVuidManagerTests.swift */, + 84861819286D188B00B7F41B /* OdpZaiusGraphQLApiManagerTests.swift */, + 8486181A286D188B00B7F41B /* OdpZaiusRestApiManagerTests.swift */, 6E27EC9A266EF11000B4A6D4 /* OptimizelyDecisionTests.swift */, 6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */, 6EC6DD6824AE94820017D296 /* OptimizelyUserContextTests.swift */, - 6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */, - 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift */, - 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift */, 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */, 6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */, 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */, + 6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */, + 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */, + 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */, + 84958C5D280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift */, 6E86CEB224FF20DE005DAFED /* OptimizelyUserContextTests_Objc.m */, ); path = "OptimizelyTests-Common"; @@ -2973,25 +2993,26 @@ isa = PBXGroup; children = ( 84640880281320F000CCF97D /* IntegrationTests.swift */, + 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 = ""; @@ -4033,19 +4054,20 @@ files = ( 6E14CDAB2423F9EB00010234 /* MockUrlSession.swift in Sources */, 6E14CDAA2423F9C300010234 /* SDKVersion.swift in Sources */, + 845945C3287758A100D13E11 /* OdpConfig.swift in Sources */, 6E14CD832423F9A100010234 /* DataStoreQueueStackImpl.swift in Sources */, 848617F12863E21200B7F41B /* ZaiusRestApiManager.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 */, + 848617CF2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E14CD952423F9A700010234 /* Group.swift in Sources */, - 84E2E96828540B5E001114AB /* OptimizelyODPConfig.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 */, + 84861802286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E14CD8B2423F9A100010234 /* UserAttribute.swift in Sources */, 84E7ABC227D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E14CD702423F94800010234 /* OptimizelyLogLevel.swift in Sources */, @@ -4073,7 +4095,7 @@ C78CAFA824486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E14CD932423F9A700010234 /* Experiment.swift in Sources */, 6E14CD982423F9C300010234 /* BackgroundingCallbacks.swift in Sources */, - 6E6522E5278E4F3800954EA1 /* ODPManager.swift in Sources */, + 6E6522E5278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E623F07253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5C2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E14CDA52423F9C300010234 /* MurmurHash3.swift in Sources */, @@ -4090,7 +4112,7 @@ 6E14CDA92423F9C300010234 /* Utils.swift in Sources */, 6EF8DE1F24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E14CD882423F9A100010234 /* AttributeValue.swift in Sources */, - 84E2E9492852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E9492852A378001114AB /* OdpVuidManager.swift in Sources */, 6E14CD822423F9A100010234 /* DataStoreFile.swift in Sources */, 6E14CDA42423F9C300010234 /* Notifications.swift in Sources */, 6E20050B26B4D28500278087 /* MockLogger.swift in Sources */, @@ -4108,7 +4130,7 @@ 6E14CD8D2423F9A700010234 /* ProjectConfig.swift in Sources */, 0B97DD9F249D4A23003DE606 /* SemanticVersion.swift in Sources */, 6E14CD8F2423F9A700010234 /* Rollout.swift in Sources */, - 84E2E9792855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E9792855875E001114AB /* OdpEventManager.swift in Sources */, 6E14CD892423F9A100010234 /* ConditionLeaf.swift in Sources */, 6E14CD9F2423F9C300010234 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E14CD9C2423F9C300010234 /* OPTDecisionService.swift in Sources */, @@ -4122,7 +4144,7 @@ 6E14CD7A2423F98D00010234 /* OPTUserProfileService.swift in Sources */, 6E14CDA32423F9C300010234 /* Constants.swift in Sources */, 6E4544B1270E67C800F2CEBC /* NetworkReachability.swift in Sources */, - 6E652307278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E652307278E688B00954EA1 /* LruCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4145,16 +4167,16 @@ 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, 848617E02863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */, - 84E2E96728540B5E001114AB /* OptimizelyODPConfig.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 */, + 84E2E9782855875E001114AB /* OdpEventManager.swift in Sources */, 6E424CFA26324B620081004A /* DataStoreMemory.swift in Sources */, 6E424CFB26324B620081004A /* DataStoreUserDefaults.swift in Sources */, - 6E6522E4278E4F3800954EA1 /* ODPManager.swift in Sources */, + 6E6522E4278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */, 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */, 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */, @@ -4163,7 +4185,7 @@ 6E424D0126324B620081004A /* SemanticVersion.swift in Sources */, 6E424D0226324B620081004A /* Audience.swift in Sources */, 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, - 84E2E9482852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E9482852A378001114AB /* OdpVuidManager.swift in Sources */, 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */, 6E424D0526324B620081004A /* ConditionHolder.swift in Sources */, 6E424D5226324C4D0081004A /* OptimizelyClient+Decide.swift in Sources */, @@ -4185,6 +4207,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 */, @@ -4201,7 +4224,7 @@ 84B4D75627E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E424D7626324DBD0081004A /* AtomicArrayTests.swift in Sources */, 6E424CD326324B270081004A /* OptimizelyError.swift in Sources */, - 6E652306278E688B00954EA1 /* LRUCache.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 */, @@ -4217,12 +4240,12 @@ 6E424CBA26324B1D0081004A /* Notifications.swift in Sources */, 6E424CBB26324B1D0081004A /* MurmurHash3.swift in Sources */, 8464087628130D3200CCF97D /* Integration.swift in Sources */, - 848617CE2863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617CE2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 848617F02863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E424CBC26324B1D0081004A /* HandlerRegistryService.swift in Sources */, 6E424CBD26324B1D0081004A /* LogMessage.swift in Sources */, 6E424CBE26324B1D0081004A /* AtomicProperty.swift in Sources */, - 84861801286CF33700B7F41B /* ODPEvent.swift in Sources */, + 84861801286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E2F8AFF26B22E8000DCEEB9 /* ConcurrencyTests_SingleClient.swift in Sources */, 84E7ABC127D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E424CBF26324B1D0081004A /* AtomicArray.swift in Sources */, @@ -4244,7 +4267,7 @@ 6E7518C522C520D400B2B157 /* Audience.swift in Sources */, 6E7517BD22C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E7518F522C520D500B2B157 /* UserAttribute.swift in Sources */, - 84E2E9732855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E9732855875E001114AB /* OdpEventManager.swift in Sources */, 6EF8DE0D24BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6EF8DE3224BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75192522C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, @@ -4253,6 +4276,7 @@ 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 */, @@ -4269,7 +4293,7 @@ 6E75190122C520D500B2B157 /* Attribute.swift in Sources */, 848617DB2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E7516B322C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, - 84E2E9432852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E9432852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E7517D522C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75187122C520D400B2B157 /* Variation.swift in Sources */, @@ -4287,7 +4311,7 @@ 6E75172B22C520D400B2B157 /* Constants.swift in Sources */, 6E7516BF22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E424BDE263228E90081004A /* AtomicArray.swift in Sources */, - 848617FC286CF33700B7F41B /* ODPEvent.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 */, @@ -4296,7 +4320,7 @@ 6E75173722C520D400B2B157 /* MurmurHash3.swift in Sources */, 6E7517F922C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, C78CAF592445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, - 848617C92863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617C92863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E7518E922C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E75184122C520D400B2B157 /* Event.swift in Sources */, 6E7517C922C520D400B2B157 /* DefaultBucketer.swift in Sources */, @@ -4304,7 +4328,7 @@ 6E7516CB22C520D400B2B157 /* OPTLogger.swift in Sources */, 6E7517A322C520D400B2B157 /* Array+Extension.swift in Sources */, 6EC6DD4224ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, - 6E652301278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E652301278E688B00954EA1 /* LruCache.swift in Sources */, 6E75193122C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E75190D22C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 6E75194922C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -4313,11 +4337,11 @@ 6E75189522C520D400B2B157 /* Experiment.swift in Sources */, 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84B4D75127E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522DF278E4F3800954EA1 /* ODPManager.swift in Sources */, + 6E6522DF278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E7518D122C520D400B2B157 /* AttributeValue.swift in Sources */, 6E75182922C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75171F22C520D400B2B157 /* OptimizelyResult.swift in Sources */, - 84E2E96228540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, + 84E2E96228540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75170722C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75174322C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E75188922C520D400B2B157 /* Project.swift in Sources */, @@ -4334,19 +4358,20 @@ files = ( 6E75170222C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E7516BA22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 845945C7287758A300D13E11 /* OdpConfig.swift in Sources */, 6E75175622C520D400B2B157 /* LogMessage.swift in Sources */, 848617F62863E21200B7F41B /* ZaiusRestApiManager.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 */, + 848617D42863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75184822C520D400B2B157 /* Event.swift in Sources */, - 84E2E96D28540B5E001114AB /* OptimizelyODPConfig.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 */, + 84861807286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75189C22C520D400B2B157 /* Experiment.swift in Sources */, 84E7ABC727D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E75176222C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -4374,7 +4399,7 @@ C78CAFAD24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7516AE22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75195022C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 6E6522EA278E4F3800954EA1 /* ODPManager.swift in Sources */, + 6E6522EA278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E623F0C253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF612445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516D222C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4391,7 +4416,7 @@ 6E7518F022C520D500B2B157 /* ConditionHolder.swift in Sources */, 6EF8DE2424BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75183022C520D400B2B157 /* BatchEvent.swift in Sources */, - 84E2E94E2852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E94E2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75192022C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E9B117922C5487A00C22D81 /* tvOSOnlyTests.swift in Sources */, 6E20051026B4D28500278087 /* MockLogger.swift in Sources */, @@ -4409,7 +4434,7 @@ 6E75180022C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 0B97DDA3249D4A26003DE606 /* SemanticVersion.swift in Sources */, 6E9B11B322C5489500C22D81 /* MockUrlSession.swift in Sources */, - 84E2E97E2855875E001114AB /* ODPEventManager.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 */, @@ -4423,7 +4448,7 @@ 6E75194422C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75179222C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E4544B6270E67C800F2CEBC /* NetworkReachability.swift in Sources */, - 6E65230C278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E65230C278E688B00954EA1 /* LruCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4435,6 +4460,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 */, @@ -4457,13 +4483,13 @@ 0B97DDA0249D4A24003DE606 /* SemanticVersion.swift in Sources */, 6E7518D422C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7518BC22C520D400B2B157 /* Variable.swift in Sources */, - 84861803286CF33700B7F41B /* ODPEvent.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 */, + 84E2E94A2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E994B3A25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75170A22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E9B11AC22C5489300C22D81 /* OTUtils.swift in Sources */, @@ -4471,7 +4497,7 @@ 6E75180822C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7518EC22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E7516AA22C520D400B2B157 /* DefaultLogger.swift in Sources */, - 84E2E97A2855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E97A2855875E001114AB /* OdpEventManager.swift in Sources */, 6E75186822C520D400B2B157 /* Rollout.swift in Sources */, 6E9B11E122C548A200C22D81 /* OptimizelyClientTests_ObjcOthers.m in Sources */, 6E623F08253F9045000617D0 /* DecisionInfo.swift in Sources */, @@ -4483,7 +4509,7 @@ 6E9B11D722C548A200C22D81 /* OptimizelyErrorTests.swift in Sources */, C78CAF8624485029009FE876 /* OptimizelyClientTests_OptimizelyJSON_Objc.m in Sources */, 6E75194C22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 6E652308278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E652308278E688B00954EA1 /* LruCache.swift in Sources */, 6E7517C022C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75183822C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75175222C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4503,18 +4529,19 @@ 6E7517FC22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E75178222C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E7518A422C520D400B2B157 /* FeatureFlag.swift in Sources */, - 6E6522E6278E4F3800954EA1 /* ODPManager.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 */, + 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 */, @@ -4529,7 +4556,7 @@ 6E9B11AB22C5489300C22D81 /* MockUrlSession.swift in Sources */, 6E75190422C520D500B2B157 /* Attribute.swift in Sources */, 6E75193422C520D500B2B157 /* OPTDataStore.swift in Sources */, - 84E2E96928540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, + 84E2E96928540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75182C22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75175E22C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E9B11DE22C548A200C22D81 /* OptimizelyClientTests_Others.swift in Sources */, @@ -4552,7 +4579,7 @@ 6EC6DD4A24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75170122C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 8464087B28130D3200CCF97D /* Integration.swift in Sources */, - 84E2E96C28540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, + 84E2E96C28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6EF8DE3A24BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E7516B922C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E75175522C520D400B2B157 /* LogMessage.swift in Sources */, @@ -4572,9 +4599,10 @@ 848617E52863E21200B7F41B /* ZaiusGraphQLApiManager.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 */, + 6E6522E9278E4F3800954EA1 /* OdpManager.swift in Sources */, 6EF8DE2324BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7517C322C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, 6E75190722C520D500B2B157 /* Attribute.swift in Sources */, @@ -4582,14 +4610,14 @@ 6E7518BF22C520D400B2B157 /* Variable.swift in Sources */, 6E75181722C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75185322C520D400B2B157 /* ProjectConfig.swift in Sources */, - 84E2E94D2852A378001114AB /* ODPVUIDManager.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 */, + 6E65230B278E688B00954EA1 /* LruCache.swift in Sources */, 6E9B11B222C5489400C22D81 /* OTUtils.swift in Sources */, 6E7518D722C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516AD22C520D400B2B157 /* DefaultLogger.swift in Sources */, @@ -4626,7 +4654,7 @@ 6E7517CF22C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6E7517FF22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 6E34A61E2319EBB800BAE302 /* Notifications.swift in Sources */, - 848617D32863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617D32863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E8A3D4F2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E4544B5270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E7517DB22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, @@ -4637,8 +4665,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 */, + 84E2E97D2855875E001114AB /* OdpEventManager.swift in Sources */, + 84861806286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75179122C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4652,16 +4680,16 @@ 848617F72863E21200B7F41B /* ZaiusRestApiManager.swift in Sources */, 6E9B116122C5487100C22D81 /* OptimizelyErrorTests.swift in Sources */, 6E9B116522C5487100C22D81 /* BatchEventBuilderTest.swift in Sources */, - 84861808286CF33700B7F41B /* ODPEvent.swift in Sources */, + 84861808286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E9B117422C5487100C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B116E22C5487100C22D81 /* LoggerTests.swift in Sources */, - 84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.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 */, + 84861818286D0B8900B7F41B /* OdpEventManagerTests.swift in Sources */, 84958C5F280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift in Sources */, 6E86CEAD24FDC84A005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75184922C520D400B2B157 /* Event.swift in Sources */, @@ -4684,12 +4712,12 @@ 6EF8DE2524BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75194522C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185522C520D400B2B157 /* ProjectConfig.swift in Sources */, - 84F6BAB427FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.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 */, + 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 */, @@ -4703,14 +4731,14 @@ 6E7516EB22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75188522C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E75176F22C520D400B2B157 /* Utils.swift in Sources */, - 6E65230D278E688B00954EA1 /* LRUCache.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 /* ODPZaiusRestApiManagerTests.swift in Sources */, + 8486181E286D188B00B7F41B /* OdpZaiusRestApiManagerTests.swift in Sources */, 6E75186122C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E75172722C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E7518FD22C520D500B2B157 /* UserAttribute.swift in Sources */, @@ -4720,13 +4748,14 @@ 6E9B117222C5487100C22D81 /* EventDispatcherTests.swift in Sources */, 6E9B116922C5487100C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E75192122C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, - 84861814286D0B8900B7F41B /* ODPManagerTests.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 /* OptimizelyODPConfig.swift in Sources */, + 84E2E96E28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E9B117322C5487100C22D81 /* BatchEventBuilderTests_Attributes.swift in Sources */, 6E9B11B622C5489600C22D81 /* OTUtils.swift in Sources */, 6E75183122C520D400B2B157 /* BatchEvent.swift in Sources */, @@ -4734,14 +4763,14 @@ 6E9B117822C5487100C22D81 /* DataStoreTests.swift in Sources */, 6E75171B22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75195122C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, - 84E2E94F2852A378001114AB /* ODPVUIDManager.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 /* ODPZaiusGraphQLApiManagerTests.swift in Sources */, + 8486181C286D188B00B7F41B /* OdpZaiusGraphQLApiManagerTests.swift in Sources */, 6E7518A922C520D400B2B157 /* FeatureFlag.swift in Sources */, 84B4D75D27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E0A72D526C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift in Sources */, @@ -4757,17 +4786,17 @@ 848617E72863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E994B3F25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75174B22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 6E6522EB278E4F3800954EA1 /* ODPManager.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 */, + 84E2E97F2855875E001114AB /* OdpEventManager.swift in Sources */, 6E9B116722C5487100C22D81 /* BatchEventBuilderTests_Events.swift in Sources */, 6E75181922C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6E75186D22C520D400B2B157 /* Rollout.swift in Sources */, 6E4544B7270E67C800F2CEBC /* NetworkReachability.swift in Sources */, - 848617D52863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617D52863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E7518F122C520D500B2B157 /* ConditionHolder.swift in Sources */, 6E20051126B4D28600278087 /* MockLogger.swift in Sources */, 6E7516DF22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, @@ -4775,7 +4804,7 @@ 6E7518B522C520D400B2B157 /* Group.swift in Sources */, 6E9B116B22C5487100C22D81 /* NotificationCenterTests.swift in Sources */, 6E7516F722C520D400B2B157 /* OptimizelyError.swift in Sources */, - 84861812286D0B8900B7F41B /* ODPSegmentManagerTests.swift in Sources */, + 84861812286D0B8900B7F41B /* OdpSegmentManagerTests.swift in Sources */, 6E75189122C520D400B2B157 /* Project.swift in Sources */, 6E7517F522C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E0207A9272A11CF008C3711 /* NetworkReachabilityTests.swift in Sources */, @@ -4799,20 +4828,21 @@ 6E75170422C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187A22C520D400B2B157 /* Variation.swift in Sources */, 0B97DD9A249D332C003DE606 /* SemanticVersionTests.swift in Sources */, - 6E65230E278E688B00954EA1 /* LRUCache.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 */, + 84861809286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75171022C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C822C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, - 84E2E9502852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E9502852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75194622C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185622C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20051226B4D28600278087 /* MockLogger.swift in Sources */, @@ -4849,7 +4879,7 @@ 6E9B119722C5488300C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184A22C520D400B2B157 /* Event.swift in Sources */, 6E75191622C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, - 848617D62863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617D62863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E9B11A522C5488300C22D81 /* ConditionHolderTests_Evaluate.swift in Sources */, 84E7ABC927D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E9B119122C5488300C22D81 /* EventForDispatchTests.swift in Sources */, @@ -4863,14 +4893,14 @@ 6E7518E622C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75179422C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11B722C5489600C22D81 /* MockUrlSession.swift in Sources */, - 6E6522EC278E4F3800954EA1 /* ODPManager.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 /* OptimizelyODPConfig.swift in Sources */, + 84E2E96F28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E7517A022C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517AC22C520D400B2B157 /* Array+Extension.swift in Sources */, 6EA425A52218E6AE00B074B5 /* (null) in Sources */, @@ -4879,7 +4909,7 @@ 6E9B11B822C5489600C22D81 /* OTUtils.swift in Sources */, 6E9B119022C5488300C22D81 /* AttributeValueTests.swift in Sources */, 6E994B4025A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 84E2E9802855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E9802855875E001114AB /* OdpEventManager.swift in Sources */, 6E75175822C520D400B2B157 /* LogMessage.swift in Sources */, 6E9B119422C5488300C22D81 /* VariableTests.swift in Sources */, 6E9B11A222C5488300C22D81 /* ConditionHolderTests.swift in Sources */, @@ -4910,7 +4940,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 84F6BAB327FCC5CF004BE62A /* OptimizelyUserContextTests_Segments.swift in Sources */, + 84F6BAB327FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift in Sources */, 6E7E9B372523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift in Sources */, 6E9B115622C5486E00C22D81 /* DecisionListenerTests.swift in Sources */, 6E9B114722C5486E00C22D81 /* OptimizelyErrorTests.swift in Sources */, @@ -4933,9 +4963,10 @@ 84958C5E280F22FE008655C7 /* OptimizelyUserContextTests_Performance.swift in Sources */, 6E7516C122C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, 6E75178122C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, - 84861800286CF33700B7F41B /* ODPEvent.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 */, @@ -4953,38 +4984,38 @@ 6E7516D922C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E7516E522C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7E9B552523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */, - 6E652305278E688B00954EA1 /* LRUCache.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 */, + 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 */, + 6E6522E3278E4F3800954EA1 /* OdpManager.swift in Sources */, 6EA2CC272345618E001E7531 /* OptimizelyConfig.swift in Sources */, - 84861815286D0B8900B7F41B /* ODPVUIDManagerTests.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 */, 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, - 84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_Segments_Decide.swift in Sources */, - 84E2E9472852A378001114AB /* ODPVUIDManager.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 /* ODPZaiusGraphQLApiManagerTests.swift in Sources */, + 8486181B286D188B00B7F41B /* OdpZaiusGraphQLApiManagerTests.swift in Sources */, 6E8A3D492637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E981FC2232C363300FADDD6 /* DecisionListenerTests_Datafile.swift in Sources */, 6E9B114A22C5486E00C22D81 /* BucketTests_Others.swift in Sources */, @@ -4999,9 +5030,9 @@ 6E9B115E22C5486E00C22D81 /* DataStoreTests.swift in Sources */, 6E4544AF270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 6E75177522C520D400B2B157 /* SDKVersion.swift in Sources */, - 848617CD2863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617CD2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75180722C520D400B2B157 /* DataStoreFile.swift in Sources */, - 8486181D286D188B00B7F41B /* ODPZaiusRestApiManagerTests.swift in Sources */, + 8486181D286D188B00B7F41B /* OdpZaiusRestApiManagerTests.swift in Sources */, 6E75183722C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B115D22C5486E00C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */, 6E75173922C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5020,7 +5051,7 @@ 6E9B115522C5486E00C22D81 /* BucketTests_BucketVariation.swift in Sources */, 6E994B3825A3E6EA00999262 /* DecisionResponse.swift in Sources */, 848617DF2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, - 84861813286D0B8900B7F41B /* ODPManagerTests.swift in Sources */, + 84861813286D0B8900B7F41B /* OdpManagerTests.swift in Sources */, 6E7516FD22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187322C520D400B2B157 /* Variation.swift in Sources */, 6E7517E322C520D400B2B157 /* DefaultDecisionService.swift in Sources */, @@ -5033,16 +5064,16 @@ 6E7518BB22C520D400B2B157 /* Variable.swift in Sources */, 6E7518AF22C520D400B2B157 /* Group.swift in Sources */, 6EF8DE3524BF7D69008B9488 /* DecisionReasons.swift in Sources */, - 84E2E96628540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, - 84861817286D0B8900B7F41B /* ODPEventManagerTests.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 */, 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 */, + 8428D3D02807337400D0FB0C /* LruCacheTests.swift in Sources */, + 84E2E9772855875E001114AB /* OdpEventManager.swift in Sources */, 6E7517FB22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5063,20 +5094,21 @@ 6E7516FF22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187522C520D400B2B157 /* Variation.swift in Sources */, 0B97DD99249D332C003DE606 /* SemanticVersionTests.swift in Sources */, - 6E652309278E688B00954EA1 /* LRUCache.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 */, + 84861804286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75170B22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7516C322C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, - 84E2E94B2852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E94B2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75194122C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185122C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E20050D26B4D28500278087 /* MockLogger.swift in Sources */, @@ -5113,7 +5145,7 @@ 6E9B118122C5488100C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184522C520D400B2B157 /* Event.swift in Sources */, 6E75191122C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, - 848617D12863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617D12863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E9B118F22C5488100C22D81 /* ConditionHolderTests_Evaluate.swift in Sources */, 84E7ABC427D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E9B117B22C5488100C22D81 /* EventForDispatchTests.swift in Sources */, @@ -5127,14 +5159,14 @@ 6E7518E122C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75178F22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E9B11AD22C5489300C22D81 /* MockUrlSession.swift in Sources */, - 6E6522E7278E4F3800954EA1 /* ODPManager.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 /* OptimizelyODPConfig.swift in Sources */, + 84E2E96A28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75179B22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517A722C520D400B2B157 /* Array+Extension.swift in Sources */, 6EA425962218E6AD00B074B5 /* (null) in Sources */, @@ -5143,7 +5175,7 @@ 6E9B11AE22C5489300C22D81 /* OTUtils.swift in Sources */, 6E9B117A22C5488100C22D81 /* AttributeValueTests.swift in Sources */, 6E994B3B25A3E6EA00999262 /* DecisionResponse.swift in Sources */, - 84E2E97B2855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E97B2855875E001114AB /* OdpEventManager.swift in Sources */, 6E75175322C520D400B2B157 /* LogMessage.swift in Sources */, 6E9B117E22C5488100C22D81 /* VariableTests.swift in Sources */, 6E9B118C22C5488100C22D81 /* ConditionHolderTests.swift in Sources */, @@ -5179,13 +5211,13 @@ 6E75176C22C520D400B2B157 /* Utils.swift in Sources */, 6E7516C422C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, C78CAFAB24486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, - 84E2E96B28540B5E001114AB /* OptimizelyODPConfig.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 */, + 6E6522E8278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E75189A22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3924ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E75179C22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -5207,7 +5239,7 @@ 6E7517E622C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171822C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174822C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 84E2E94C2852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E94C2852A378001114AB /* OdpVuidManager.swift in Sources */, 6E7518FA22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7516E822C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75191222C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, @@ -5215,9 +5247,9 @@ 6E75182E22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7516DC22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E75182222C520D400B2B157 /* BatchEventBuilder.swift in Sources */, - 84861805286CF33700B7F41B /* ODPEvent.swift in Sources */, + 84861805286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75190622C520D500B2B157 /* Attribute.swift in Sources */, - 848617D22863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617D22863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75183A22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6EA2CC2A2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E9B11B022C5489400C22D81 /* OTUtils.swift in Sources */, @@ -5243,6 +5275,7 @@ 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 */, @@ -5251,7 +5284,7 @@ 6E7518D622C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7518A622C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75186A22C520D400B2B157 /* Rollout.swift in Sources */, - 84E2E97C2855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E97C2855875E001114AB /* OdpEventManager.swift in Sources */, 6E75178422C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EF8DE1424BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75175422C520D400B2B157 /* LogMessage.swift in Sources */, @@ -5261,7 +5294,7 @@ 6E7517A822C520D400B2B157 /* Array+Extension.swift in Sources */, 6E7518EE22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E75185222C520D400B2B157 /* ProjectConfig.swift in Sources */, - 6E65230A278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E65230A278E688B00954EA1 /* LruCache.swift in Sources */, C78CAF5F2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E623F0A253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75179022C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, @@ -5279,13 +5312,13 @@ 6E75177122C520D400B2B157 /* Utils.swift in Sources */, 6E7516C922C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, C78CAFB024486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, - 84E2E97028540B5E001114AB /* OptimizelyODPConfig.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 */, + 6E6522ED278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E75189F22C520D400B2B157 /* Experiment.swift in Sources */, 6EC6DD3E24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, 6E7517A122C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, @@ -5307,7 +5340,7 @@ 6E7517EB22C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171D22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174D22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, - 84E2E9512852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E9512852A378001114AB /* OdpVuidManager.swift in Sources */, 6E7518FF22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7516ED22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75191722C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, @@ -5315,9 +5348,9 @@ 6E75183322C520D400B2B157 /* BatchEvent.swift in Sources */, 6E7516E122C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E75182722C520D400B2B157 /* BatchEventBuilder.swift in Sources */, - 8486180A286CF33700B7F41B /* ODPEvent.swift in Sources */, + 8486180A286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75190B22C520D500B2B157 /* Attribute.swift in Sources */, - 848617D72863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617D72863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75183F22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6EA2CC2F2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E9B11BA22C5489700C22D81 /* OTUtils.swift in Sources */, @@ -5343,6 +5376,7 @@ 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 */, @@ -5351,7 +5385,7 @@ 6E7518DB22C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7518AB22C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75186F22C520D400B2B157 /* Rollout.swift in Sources */, - 84E2E9812855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E9812855875E001114AB /* OdpEventManager.swift in Sources */, 6E75178922C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EF8DE1924BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E75175922C520D400B2B157 /* LogMessage.swift in Sources */, @@ -5361,7 +5395,7 @@ 6E7517AD22C520D400B2B157 /* Array+Extension.swift in Sources */, 6E7518F322C520D500B2B157 /* ConditionHolder.swift in Sources */, 6E75185722C520D400B2B157 /* ProjectConfig.swift in Sources */, - 6E65230F278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E65230F278E688B00954EA1 /* LruCache.swift in Sources */, C78CAF642445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E623F0F253F9045000617D0 /* DecisionInfo.swift in Sources */, 6E75179522C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, @@ -5379,7 +5413,7 @@ 6E75190022C520D500B2B157 /* Attribute.swift in Sources */, 6E7518AC22C520D400B2B157 /* Group.swift in Sources */, 6E7517C822C520D400B2B157 /* DefaultBucketer.swift in Sources */, - 84E2E9722855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E9722855875E001114AB /* OdpEventManager.swift in Sources */, 6EF8DE0C24BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6EF8DE3124BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E7517BC22C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */, @@ -5388,6 +5422,7 @@ 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 */, @@ -5404,7 +5439,7 @@ 6E75195422C520D500B2B157 /* OPTBucketer.swift in Sources */, 848617DA2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, 6E75171E22C520D400B2B157 /* OptimizelyResult.swift in Sources */, - 84E2E9422852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E9422852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75189422C520D400B2B157 /* Experiment.swift in Sources */, @@ -5422,7 +5457,7 @@ 6E75194822C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7518E822C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E424BDD263228E90081004A /* AtomicArray.swift in Sources */, - 848617FB286CF33700B7F41B /* ODPEvent.swift in Sources */, + 848617FB286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75191822C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E7518B822C520D400B2B157 /* Variable.swift in Sources */, 6E75173622C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -5431,7 +5466,7 @@ 6E75186422C520D400B2B157 /* Rollout.swift in Sources */, 6E75179622C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, C78CAF582445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, - 848617C82863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617C82863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75170622C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E7518A022C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75174222C520D400B2B157 /* HandlerRegistryService.swift in Sources */, @@ -5439,7 +5474,7 @@ 6E75184C22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E75181022C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD4124ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, - 6E652300278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E652300278E688B00954EA1 /* LruCache.swift in Sources */, 6E7516EE22C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75183422C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75171222C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -5448,11 +5483,11 @@ 6E75192422C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84B4D75027E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522DE278E4F3800954EA1 /* ODPManager.swift in Sources */, + 6E6522DE278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E75193022C520D500B2B157 /* OPTDataStore.swift in Sources */, 6E7517E022C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E7516FA22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, - 84E2E96128540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, + 84E2E96128540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75187C22C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E7517EC22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E75174E22C520D400B2B157 /* LogMessage.swift in Sources */, @@ -5469,19 +5504,20 @@ files = ( 6E7516FC22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E7516B422C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 845945C02877589F00D13E11 /* OdpConfig.swift in Sources */, 6E75175022C520D400B2B157 /* LogMessage.swift in Sources */, 848617EE2863E21200B7F41B /* ZaiusRestApiManager.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 */, + 848617CC2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75184222C520D400B2B157 /* Event.swift in Sources */, - 84E2E96528540B5E001114AB /* OptimizelyODPConfig.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 */, + 848617FF286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E75189622C520D400B2B157 /* Experiment.swift in Sources */, 84E7ABBF27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */, 6E75175C22C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -5509,7 +5545,7 @@ C78CAFA624486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E7518D222C520D400B2B157 /* AttributeValue.swift in Sources */, 6E7516A822C520D400B2B157 /* DefaultLogger.swift in Sources */, - 6E6522E2278E4F3800954EA1 /* ODPManager.swift in Sources */, + 6E6522E2278E4F3800954EA1 /* OdpManager.swift in Sources */, 6E623F05253F9045000617D0 /* DecisionInfo.swift in Sources */, C78CAF5A2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E75194A22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, @@ -5526,7 +5562,7 @@ 6E7518DE22C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6EF8DE1D24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E7518EA22C520D400B2B157 /* ConditionHolder.swift in Sources */, - 84E2E9462852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E9462852A378001114AB /* OdpVuidManager.swift in Sources */, 6E75182A22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75191A22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E20050826B4D28500278087 /* MockLogger.swift in Sources */, @@ -5544,7 +5580,7 @@ 6E7517FA22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 0B97DD9D249D4A22003DE606 /* SemanticVersion.swift in Sources */, 6E9B11A722C5489200C22D81 /* MockUrlSession.swift in Sources */, - 84E2E9762855875E001114AB /* ODPEventManager.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 */, @@ -5558,7 +5594,7 @@ 6E75193E22C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75178C22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E4544AE270E67C800F2CEBC /* NetworkReachability.swift in Sources */, - 6E652304278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E652304278E688B00954EA1 /* LruCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5588,13 +5624,13 @@ 75C71A0F25E454460084187E /* DefaultUserProfileService.swift in Sources */, 75C71A1025E454460084187E /* DefaultEventDispatcher.swift in Sources */, 75C71A1125E454460084187E /* OPTLogger.swift in Sources */, - 848617CB2863DC2700B7F41B /* ODPSegmentManager.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 */, + 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 */, @@ -5602,7 +5638,7 @@ 75C71A1925E454460084187E /* DecisionReasons.swift in Sources */, 75C71A1A25E454460084187E /* DecisionResponse.swift in Sources */, 75C71A1B25E454460084187E /* DataStoreMemory.swift in Sources */, - 84E2E96428540B5E001114AB /* OptimizelyODPConfig.swift in Sources */, + 84E2E96428540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 75C71A1C25E454460084187E /* DataStoreUserDefaults.swift in Sources */, 75C71A1D25E454460084187E /* DataStoreFile.swift in Sources */, 75C71A1E25E454460084187E /* DataStoreQueueStackImpl.swift in Sources */, @@ -5611,8 +5647,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 */, + 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 */, @@ -5641,12 +5677,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 /* ZaiusRestApiManager.swift in Sources */, - 848617FE286CF33700B7F41B /* ODPEvent.swift in Sources */, + 848617FE286CF33700B7F41B /* OdpEvent.swift in Sources */, 75C71A4225E454460084187E /* HandlerRegistryService.swift in Sources */, 75C71A4325E454460084187E /* LogMessage.swift in Sources */, 75C71A4425E454460084187E /* AtomicProperty.swift in Sources */, @@ -5665,7 +5702,7 @@ BD6485402491474500F30986 /* Attribute.swift in Sources */, BD6485412491474500F30986 /* Group.swift in Sources */, BD6485422491474500F30986 /* DefaultBucketer.swift in Sources */, - 84E2E9742855875E001114AB /* ODPEventManager.swift in Sources */, + 84E2E9742855875E001114AB /* OdpEventManager.swift in Sources */, 6EF8DE0E24BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6EF8DE3324BF7D69008B9488 /* DecisionReasons.swift in Sources */, BD6485432491474500F30986 /* DefaultDatafileHandler.swift in Sources */, @@ -5674,6 +5711,7 @@ 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 */, @@ -5690,7 +5728,7 @@ BD6485502491474500F30986 /* OPTBucketer.swift in Sources */, 848617DC2863E21200B7F41B /* ZaiusGraphQLApiManager.swift in Sources */, BD6485512491474500F30986 /* OptimizelyResult.swift in Sources */, - 84E2E9442852A378001114AB /* ODPVUIDManager.swift in Sources */, + 84E2E9442852A378001114AB /* OdpVuidManager.swift in Sources */, BD6485522491474500F30986 /* Constants.swift in Sources */, BD6485532491474500F30986 /* DefaultLogger.swift in Sources */, BD6485542491474500F30986 /* Experiment.swift in Sources */, @@ -5708,7 +5746,7 @@ BD64855E2491474500F30986 /* OPTDatafileHandler.swift in Sources */, BD64855F2491474500F30986 /* ConditionHolder.swift in Sources */, 6E424BDF263228E90081004A /* AtomicArray.swift in Sources */, - 848617FD286CF33700B7F41B /* ODPEvent.swift in Sources */, + 848617FD286CF33700B7F41B /* OdpEvent.swift in Sources */, BD6485602491474500F30986 /* OPTNotificationCenter.swift in Sources */, BD6485612491474500F30986 /* Variable.swift in Sources */, BD6485622491474500F30986 /* MurmurHash3.swift in Sources */, @@ -5717,7 +5755,7 @@ BD6485642491474500F30986 /* Rollout.swift in Sources */, BD6485652491474500F30986 /* DataStoreQueueStackImpl+Extension.swift in Sources */, BD6485662491474500F30986 /* OptimizelyJSON.swift in Sources */, - 848617CA2863DC2700B7F41B /* ODPSegmentManager.swift in Sources */, + 848617CA2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, BD6485672491474500F30986 /* OptimizelyClient.swift in Sources */, BD6485682491474500F30986 /* FeatureFlag.swift in Sources */, BD6485692491474500F30986 /* HandlerRegistryService.swift in Sources */, @@ -5725,7 +5763,7 @@ BD64856B2491474500F30986 /* ProjectConfig.swift in Sources */, BD64856C2491474500F30986 /* DataStoreQueueStackImpl.swift in Sources */, 6EC6DD4324ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, - 6E652302278E688B00954EA1 /* LRUCache.swift in Sources */, + 6E652302278E688B00954EA1 /* LruCache.swift in Sources */, BD64856D2491474500F30986 /* OptimizelyError.swift in Sources */, BD64856E2491474500F30986 /* EventForDispatch.swift in Sources */, BD64856F2491474500F30986 /* OptimizelyClient+ObjC.swift in Sources */, @@ -5734,11 +5772,11 @@ BD6485722491474500F30986 /* DataStoreQueueStack.swift in Sources */, BD6485732491474500F30986 /* OptimizelyConfig.swift in Sources */, 84B4D75227E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, - 6E6522E0278E4F3800954EA1 /* ODPManager.swift in Sources */, + 6E6522E0278E4F3800954EA1 /* OdpManager.swift in Sources */, BD6485742491474500F30986 /* OPTDataStore.swift in Sources */, BD6485752491474500F30986 /* DefaultDecisionService.swift in Sources */, BD6485762491474500F30986 /* OptimizelyLogLevel.swift in Sources */, - 84E2E96328540B5E001114AB /* OptimizelyODPConfig.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/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index 738cf0a2..20a38b10 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -56,7 +56,7 @@ extension OptimizelyClient { /// Set this to 0 to disable periodic downloading. /// - defaultLogLevel: default log level (optional. default = .info) /// - defaultDecisionOptions: default decision optiopns (optional) - /// - odpConfig: ODP configuration (optional) + /// - settings: SDK configuration (optional) public convenience init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, @@ -65,7 +65,7 @@ extension OptimizelyClient { periodicDownloadInterval: Int?, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - odpConfig: OptimizelyODPConfig? = nil) { + settings: OptimizelySdkSettings? = nil) { self.init(sdkKey: sdkKey, logger: logger, @@ -74,7 +74,7 @@ extension OptimizelyClient { userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, defaultDecideOptions: defaultDecideOptions, - odpConfig: odpConfig) + settings: settings) let interval = periodicDownloadInterval ?? 10 * 60 if interval > 0 { diff --git a/Sources/ODP/LRUCache.swift b/Sources/ODP/LruCache.swift similarity index 99% rename from Sources/ODP/LRUCache.swift rename to Sources/ODP/LruCache.swift index f18d2850..56dd9b37 100644 --- a/Sources/ODP/LRUCache.swift +++ b/Sources/ODP/LruCache.swift @@ -16,7 +16,7 @@ import Foundation -class LRUCache { +class LruCache { class CacheElement { var prev: CacheElement? diff --git a/Sources/ODP/ODPManager.swift b/Sources/ODP/ODPManager.swift deleted file mode 100644 index d4288e4f..00000000 --- a/Sources/ODP/ODPManager.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// 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 { - let odpConfig: OptimizelyODPConfig - - let vuidManager: ODPVUIDManager - let segmentManager: ODPSegmentManager - let eventManager: ODPEventManager - - let logger = OPTLoggerFactory.getLogger() - - init(sdkKey: String, - odpConfig: OptimizelyODPConfig, - vuidManager: ODPVUIDManager? = nil, - segmentManager: ODPSegmentManager? = nil, - eventManager: ODPEventManager? = nil) { - self.odpConfig = odpConfig - self.vuidManager = vuidManager ?? ODPVUIDManager.shared - self.segmentManager = segmentManager ?? ODPSegmentManager(odpConfig: odpConfig) - self.eventManager = eventManager ?? ODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) - - self.eventManager.registerVUID(vuid: self.vuidManager.vuid) - } - - func fetchQualifiedSegments(userId: String, - segmentsToCheck: [String], - options: [OptimizelySegmentOption], - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - let userKey = vuidManager.isVuid(visitorId: userId) ? Constants.ODP.keyForVuid : Constants.ODP.keyForUserId - let userValue = userId - - segmentManager.fetchQualifiedSegments(userKey: userKey, - userValue: userValue, - segmentsToCheck: segmentsToCheck, - options: options, - completionHandler: completionHandler) - } - - func identifyUser(userId: String) { - eventManager.identifyUser(vuid: vuidManager.vuid, userId: userId) - } - - func sendEvent(type: String, action: String, identifiers: [String: String], data: [String: Any]) { - 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?) { - odpConfig.update(apiKey: apiKey, apiHost: apiHost) - eventManager.flush() - } - - var vuid: String { - return vuidManager.vuid - } - -} diff --git a/Sources/ODP/OptimizelyODPConfig.swift b/Sources/ODP/OdpConfig.swift similarity index 72% rename from Sources/ODP/OptimizelyODPConfig.swift rename to Sources/ODP/OdpConfig.swift index 89571f91..6924a5b9 100644 --- a/Sources/ODP/OptimizelyODPConfig.swift +++ b/Sources/ODP/OdpConfig.swift @@ -16,39 +16,34 @@ import Foundation -public class OptimizelyODPConfig { - /// maximum size (default = 100) of audience segments cache (optional) - let segmentsCacheSize: Int - /// timeout in seconds (default = 600) of audience segments cache (optional) - let segmentsCacheTimeoutInSecs: Int +class OdpConfig { /// The host URL for the ODP audience segments API (optional). If not provided, SDK will use the default host in datafile. private var _apiHost: String? /// The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile. private var _apiKey: String? - /// enabled by default (disabled when datafile has no ODP key/host settings) - private var _enabled = true - + /// assumed integrated by default (set to false when datafile has no ODP key/host settings) + private var _odpServiceIntegrated = true + let queue = DispatchQueue(label: "odpConfig") - public init(segmentsCacheSize: Int = 100, - segmentsCacheTimeoutInSecs: Int = 600) { - self.segmentsCacheSize = segmentsCacheSize - self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs + init(apiKey: String? = nil, apiHost: String? = nil) { + self._apiKey = apiKey + self._apiHost = apiHost } - + func update(apiKey: String?, apiHost: String?) { self.apiKey = apiKey self.apiHost = apiHost - // disable future event queueing if datafile has no ODP settings + // disable future event queueing if datafile has no ODP integrations. - self.enabled = (apiKey != nil) && (apiHost != nil) + self.odpServiceIntegrated = (apiKey != nil) && (apiHost != nil) } } // MARK: - Thread-safe -extension OptimizelyODPConfig { +extension OdpConfig { var apiHost: String? { get { @@ -80,17 +75,17 @@ extension OptimizelyODPConfig { } } - var enabled: Bool { + var odpServiceIntegrated: Bool { get { var value = false queue.sync { - value = _enabled + value = _odpServiceIntegrated } return value } set { queue.async { - self._enabled = newValue + self._odpServiceIntegrated = newValue } } } diff --git a/Sources/ODP/ODPEvent.swift b/Sources/ODP/OdpEvent.swift similarity index 95% rename from Sources/ODP/ODPEvent.swift rename to Sources/ODP/OdpEvent.swift index 42332fa9..9eb47ef4 100644 --- a/Sources/ODP/ODPEvent.swift +++ b/Sources/ODP/OdpEvent.swift @@ -16,7 +16,7 @@ import Foundation -struct ODPEvent: Codable { +struct OdpEvent: Codable { let type: String let action: String let identifiers: [String: String] @@ -68,9 +68,9 @@ struct ODPEvent: Codable { } -extension ODPEvent: Equatable { +extension OdpEvent: Equatable { - public static func == (lhs: ODPEvent, rhs: ODPEvent) -> Bool { + public static func == (lhs: OdpEvent, rhs: OdpEvent) -> Bool { return lhs.type == rhs.type && lhs.action == rhs.action && lhs.identifiers == rhs.identifiers && diff --git a/Sources/ODP/ODPEventManager.swift b/Sources/ODP/OdpEventManager.swift similarity index 87% rename from Sources/ODP/ODPEventManager.swift rename to Sources/ODP/OdpEventManager.swift index 4004962b..7a7e19de 100644 --- a/Sources/ODP/ODPEventManager.swift +++ b/Sources/ODP/OdpEventManager.swift @@ -17,26 +17,26 @@ import Foundation import UIKit -class ODPEventManager { - let odpConfig: OptimizelyODPConfig +class OdpEventManager { + let odpConfig: OdpConfig let zaiusMgr: ZaiusRestApiManager let maxQueueSize = 100 let maxFailureCount = 3 let queueLock: DispatchQueue - let eventQueue: DataStoreQueueStackImpl + let eventQueue: DataStoreQueueStackImpl let logger = OPTLoggerFactory.getLogger() - init(sdkKey: String, odpConfig: OptimizelyODPConfig, apiManager: ZaiusRestApiManager? = nil) { + init(sdkKey: String, odpConfig: OdpConfig, apiManager: ZaiusRestApiManager? = nil) { self.odpConfig = odpConfig self.zaiusMgr = apiManager ?? ZaiusRestApiManager() self.queueLock = DispatchQueue(label: "event") // a separate event queue for each sdkKey (which may have own ODP public key) - let storeName = "OPDEvent-\(sdkKey)" - self.eventQueue = DataStoreQueueStackImpl(queueStackName: "odp", + let storeName = "OPTEvent-ODP-\(sdkKey)" + self.eventQueue = DataStoreQueueStackImpl(queueStackName: "odp", dataStore: DataStoreFile<[Data]>(storeName: storeName)) } @@ -62,7 +62,7 @@ class ODPEventManager { } func sendEvent(type: String, action: String, identifiers: [String: String], data: [String: Any]) { - let event = ODPEvent(type: type, + let event = OdpEvent(type: type, action: action, identifiers: identifiers, data: addCommonEventData(data)) @@ -93,8 +93,9 @@ class ODPEventManager { // MARK: - dispatch - func dispatch(_ event: ODPEvent) { - guard odpConfig.enabled else { + func dispatch(_ event: OdpEvent) { + // do not queue events if datafile has no ODP public key (not integrated) + guard odpConfig.odpServiceIntegrated else { logger.d("ODP has been disabled.") return } @@ -109,14 +110,18 @@ class ODPEventManager { flush() } + func clearEvents() { + + } + func flush() { + guard odpConfig.odpServiceIntegrated else { + // clean up all pending events if datafile has no ODP public key (not integrated) + _ = eventQueue.removeFirstItems(count: self.maxQueueSize) + return + } + guard let odpApiKey = odpConfig.apiKey, let odpApiHost = odpConfig.apiHost else { - if !odpConfig.enabled { - queueLock.async { - // clean up pending events if ODP is disabled - _ = self.eventQueue.removeFirstItems(count: self.maxQueueSize) - } - } return } @@ -137,7 +142,7 @@ class ODPEventManager { let maxBatchEvents = 10 var failureCount = 0 - while let events: [ODPEvent] = self.eventQueue.getFirstItems(count: maxBatchEvents) { + while let events: [OdpEvent] = self.eventQueue.getFirstItems(count: maxBatchEvents) { let numEvents = events.count // we've exhuasted our failure count. Give up and try the next time a event @@ -150,7 +155,7 @@ class ODPEventManager { // make the send event synchronous. enter our notify notify.enter() - self.zaiusMgr.sendODPEvents(apiKey: odpApiKey, + self.zaiusMgr.sendOdpEvents(apiKey: odpApiKey, apiHost: odpApiHost, events: events) { error in defer { diff --git a/Sources/ODP/OdpManager.swift b/Sources/ODP/OdpManager.swift new file mode 100644 index 00000000..633b7273 --- /dev/null +++ b/Sources/ODP/OdpManager.swift @@ -0,0 +1,105 @@ +// +// 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 { + let enabled: Bool + let odpConfig: OdpConfig + + let vuidManager: OdpVuidManager + var segmentManager: OdpSegmentManager? + var eventManager: OdpEventManager? + + let logger = OPTLoggerFactory.getLogger() + + var vuid: String { + return vuidManager.vuid + } + + init(sdkKey: String, + enable: Bool, + cacheSize: Int, + cacheTimeoutInSecs: Int, + segmentManager: OdpSegmentManager? = nil, + eventManager: OdpEventManager? = nil) { + + self.enabled = enable + self.odpConfig = OdpConfig() + self.vuidManager = OdpVuidManager.shared + + if enable { + self.segmentManager = segmentManager ?? OdpSegmentManager(cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeoutInSecs, + odpConfig: odpConfig) + self.eventManager = eventManager ?? OdpEventManager(sdkKey: sdkKey, odpConfig: odpConfig) + + self.eventManager?.registerVUID(vuid: self.vuidManager.vuid) + } + } + + func fetchQualifiedSegments(userId: String, + segmentsToCheck: [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, + segmentsToCheck: segmentsToCheck, + options: options, + completionHandler: completionHandler) + } + + func identifyUser(userId: String) { + guard enabled else { + logger.d("ODP is not enabled.") + return + } + + eventManager?.identifyUser(vuid: vuidManager.vuid, userId: userId) + } + + func sendEvent(type: String, action: String, identifiers: [String: String], data: [String: Any]) { + guard enabled else { + logger.d("ODP is not enabled.") + return + } + + 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?) { + guard enabled else { + return + } + + odpConfig.update(apiKey: apiKey, apiHost: apiHost) + eventManager?.flush() + } + +} diff --git a/Sources/ODP/ODPSegmentManager.swift b/Sources/ODP/OdpSegmentManager.swift similarity index 87% rename from Sources/ODP/ODPSegmentManager.swift rename to Sources/ODP/OdpSegmentManager.swift index ae23c897..e2489930 100644 --- a/Sources/ODP/ODPSegmentManager.swift +++ b/Sources/ODP/OdpSegmentManager.swift @@ -16,19 +16,22 @@ import Foundation -class ODPSegmentManager { - let odpConfig: OptimizelyODPConfig +class OdpSegmentManager { + let odpConfig: OdpConfig let zaiusMgr: ZaiusGraphQLApiManager - let segmentsCache: LRUCache + let segmentsCache: LruCache let logger = OPTLoggerFactory.getLogger() - init(odpConfig: OptimizelyODPConfig, apiManager: ZaiusGraphQLApiManager? = nil) { + init(cacheSize: Int, + cacheTimeoutInSecs: Int, + odpConfig: OdpConfig, + apiManager: ZaiusGraphQLApiManager? = nil) { self.odpConfig = odpConfig self.zaiusMgr = apiManager ?? ZaiusGraphQLApiManager() - self.segmentsCache = LRUCache(size: odpConfig.segmentsCacheSize, - timeoutInSecs: odpConfig.segmentsCacheTimeoutInSecs) + self.segmentsCache = LruCache(size: cacheSize, + timeoutInSecs: cacheTimeoutInSecs) } func fetchQualifiedSegments(userKey: String, @@ -76,7 +79,7 @@ class ODPSegmentManager { // MARK: - Utils -extension ODPSegmentManager { +extension OdpSegmentManager { func makeCacheKey(_ userKey: String, _ userValue: String) -> String { return userKey + "-$-" + userValue diff --git a/Sources/ODP/ODPVUIDManager.swift b/Sources/ODP/OdpVuidManager.swift similarity index 88% rename from Sources/ODP/ODPVUIDManager.swift rename to Sources/ODP/OdpVuidManager.swift index 942af62b..52abb65f 100644 --- a/Sources/ODP/ODPVUIDManager.swift +++ b/Sources/ODP/OdpVuidManager.swift @@ -16,29 +16,29 @@ import Foundation -class ODPVUIDManager { +class OdpVuidManager { var vuid: String = "" let logger = OPTLoggerFactory.getLogger() // a single vuid should be shared for all SDK instances - static let shared = ODPVUIDManager() + static let shared = OdpVuidManager() init() { self.vuid = load() } func makeVuid() -> String { - return "VUID_" + UUID().uuidString.replacingOccurrences(of: "-", with: "") + return "vuid_" + UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased() } func isVuid(visitorId: String) -> Bool { - return visitorId.starts(with: "VUID") + return visitorId.starts(with: "vuid") } } // MARK: - VUID Store -extension ODPVUIDManager { +extension OdpVuidManager { // UserDefaults format: (keep the most recent vuid info only) // "optimizely-odp": { diff --git a/Sources/ODP/OptimizelySdkSettings.swift b/Sources/ODP/OptimizelySdkSettings.swift new file mode 100644 index 00000000..cd0e06d8 --- /dev/null +++ b/Sources/ODP/OptimizelySdkSettings.swift @@ -0,0 +1,34 @@ +// +// 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 { + /// maximum size (default = 100) of audience segments cache (optional) + let segmentsCacheSize: Int + /// timeout in seconds (default = 600) of audience segments cache (optional) + let segmentsCacheTimeoutInSecs: Int + /// set this flag to false (default = true) to disable ODP features + let enableOdp: Bool + + public init(segmentsCacheSize: Int = 100, + segmentsCacheTimeoutInSecs: Int = 600, + enableOdp: Bool = true) { + self.segmentsCacheSize = segmentsCacheSize + self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs + self.enableOdp = enableOdp + } +} diff --git a/Sources/ODP/ZaiusGraphQLApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift index 3a86f150..f096e8e4 100644 --- a/Sources/ODP/ZaiusGraphQLApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -166,7 +166,7 @@ class ZaiusGraphQLApiManager { return } - let audiences = audDict.compactMap { ODPAudience($0["node"] as? [String: Any]) } + let audiences = audDict.compactMap { OdpAudience($0["node"] as? [String: Any]) } let segments = audiences.filter { $0.isQualified }.map { $0.name } completionHandler(segments, nil) } @@ -192,7 +192,7 @@ class ZaiusGraphQLApiManager { // MARK: - Utils -struct ODPAudience: Decodable { +struct OdpAudience: Decodable { let name: String let state: String let description: String? // optional so we can add for debugging diff --git a/Sources/ODP/ZaiusRestApiManager.swift b/Sources/ODP/ZaiusRestApiManager.swift index 9219fa95..9aceda43 100644 --- a/Sources/ODP/ZaiusRestApiManager.swift +++ b/Sources/ODP/ZaiusRestApiManager.swift @@ -32,9 +32,9 @@ import Foundation class ZaiusRestApiManager { - func sendODPEvents(apiKey: String, + func sendOdpEvents(apiKey: String, apiHost: String, - events: [ODPEvent], + events: [OdpEvent], completionHandler: @escaping (OptimizelyError?) -> Void) { guard let url = URL(string: "\(apiHost)/v3/events") else { let canRetry = false diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index d6d40d96..b546826a 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -71,7 +71,7 @@ public class OptimizelyUserContext { userId: String, attributes: [String: Any?]? = nil) { self.init(optimizely: optimizely, userId: userId, attributes: attributes ?? [:]) - self.optimizely?.registerUserToODP(userId: userId) + self.optimizely?.registerUserToOdp(userId: userId) } /// OptimizelyUserContext init for vuid-based decision diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index f99477d4..0c36ea75 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -53,7 +53,8 @@ open class OptimizelyClient: NSObject { var decisionService: OPTDecisionService! public var notificationCenter: OPTNotificationCenter? - var odpManager: ODPManager + var odpManager: OdpManager + let sdkSettings: OptimizelySdkSettings // MARK: - Public interfaces @@ -67,7 +68,7 @@ open class OptimizelyClient: NSObject { /// - userProfileService: custom UserProfileService (optional) /// - defaultLogLevel: default log level (optional. default = .info) /// - defaultDecisionOptions: default decision options (optional) - /// - odpConfig: ODP configuration (optional) + /// - settings: SDK configuration (optional) public init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, @@ -75,12 +76,16 @@ open class OptimizelyClient: NSObject { userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - odpConfig: OptimizelyODPConfig? = nil) { + settings: OptimizelySdkSettings? = nil) { self.sdkKey = sdkKey + self.sdkSettings = settings ?? OptimizelySdkSettings() self.defaultDecideOptions = defaultDecideOptions ?? [] - self.odpManager = ODPManager(sdkKey: sdkKey, - odpConfig: odpConfig ?? OptimizelyODPConfig()) + + self.odpManager = OdpManager(sdkKey: sdkKey, + enable: sdkSettings.enableOdp, + cacheSize: sdkSettings.segmentsCacheSize, + cacheTimeoutInSecs: sdkSettings.segmentsCacheTimeoutInSecs) super.init() @@ -195,7 +200,7 @@ open class OptimizelyClient: NSObject { do { self.config = try ProjectConfig(datafile: datafile) - odpManager.updateODPConfig(apiKey: config?.publicKeyForODP, apiHost: config?.hostForODP) + odpManager.updateOdpConfig(apiKey: config?.publicKeyForODP, apiHost: config?.hostForODP) datafileHandler?.startUpdates(sdkKey: self.sdkKey) { data in // new datafile came in @@ -929,7 +934,7 @@ extension OptimizelyClient { /// - 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. - public func sendODPEvent(type: String? = nil, + public func sendOdpEvent(type: String? = nil, action: String, identifiers: [String: String] = [:], data: [String: Any] = [:]) { @@ -944,7 +949,7 @@ extension OptimizelyClient { return odpManager.vuid } - func registerUserToODP(userId: String) { + func registerUserToOdp(userId: String) { odpManager.identifyUser(userId: userId) } @@ -956,6 +961,12 @@ extension OptimizelyClient { return } + // emtpy segmentsToCheck (no ODP audiences found in datafile) is not an error. + guard segmentsToCheck.count > 0 else { + completionHandler([], nil) + return + } + odpManager.fetchQualifiedSegments(userId: userId, segmentsToCheck: segmentsToCheck, options: options, diff --git a/Sources/Optimizely/OptimizelyError.swift b/Sources/Optimizely/OptimizelyError.swift index 30acfdf4..557df704 100644 --- a/Sources/Optimizely/OptimizelyError.swift +++ b/Sources/Optimizely/OptimizelyError.swift @@ -85,6 +85,7 @@ public enum OptimizelyError: Error { case invalidSegmentIdentifier case fetchSegmentsFailed(_ hint: String) case odpEventFailed(_ hint: String, _ canRetry: Bool) + case odpNotEnabled } // MARK: - CustomStringConvertible @@ -157,6 +158,7 @@ extension OptimizelyError: CustomStringConvertible, ReasonProtocol { 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 .odpNotEnabled: message = "ODP is not enabled." } return message diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift new file mode 100644 index 00000000..fbb31b9d --- /dev/null +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift @@ -0,0 +1,45 @@ +// +// 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) + } + + func testOdp() { + XCTFail() + } + +} + + +//extension OptimizelyClientTests_ODP { +// +// class MockOdpManager: OdpManager { +// +// override init( +// } +// +//} diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index 33d3ffa4..2dd574d2 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift @@ -1214,7 +1214,7 @@ class FakeManager: OptimizelyClient { userProfileService: OPTUserProfileService? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - odpConfig: OptimizelyODPConfig? = nil) { + settings: OptimizelySdkSettings? = nil) { // clear shared handlers HandlerRegistryService.shared.removeAll() @@ -1226,7 +1226,7 @@ class FakeManager: OptimizelyClient { userProfileService: userProfileService, defaultLogLevel: defaultLogLevel, defaultDecideOptions: defaultDecideOptions, - odpConfig: odpConfig) + settings: settings) let userProfileService = userProfileService ?? DefaultUserProfileService() self.decisionService = FakeDecisionService(userProfileService: userProfileService) diff --git a/Tests/OptimizelyTests-Common/LRUCacheTests.swift b/Tests/OptimizelyTests-Common/LruCacheTests.swift similarity index 85% rename from Tests/OptimizelyTests-Common/LRUCacheTests.swift rename to Tests/OptimizelyTests-Common/LruCacheTests.swift index 0f8360e2..a009ab30 100644 --- a/Tests/OptimizelyTests-Common/LRUCacheTests.swift +++ b/Tests/OptimizelyTests-Common/LruCacheTests.swift @@ -16,29 +16,21 @@ import XCTest -class LRUCacheTests: XCTestCase { +class LruCacheTests: XCTestCase { func testMinConfig() { - var cache = LRUCache(size: 1000, timeoutInSecs: 2000) + var cache = LruCache(size: 1000, timeoutInSecs: 2000) XCTAssertEqual(1000, cache.size) XCTAssertEqual(2000, cache.timeoutInSecs) - cache = LRUCache(size: 0, timeoutInSecs: 0) - XCTAssertEqual(10, cache.size) - XCTAssertEqual(60, cache.timeoutInSecs) + cache = LruCache(size: 0, timeoutInSecs: 0) + XCTAssertEqual(0, cache.size) + XCTAssertEqual(0, cache.timeoutInSecs) } - -} - -// tests below will be skipped in CI (travis/actions) since they use time control and debug-mode configs. - -#if DEBUG - -extension LRUCacheTests { func testSaveAndLookup() { let maxSize = 2 - let cache = LRUCache(size: maxSize, timeoutInSecs: 1000) + let cache = LruCache(size: maxSize, timeoutInSecs: 1000) XCTAssertNil(cache.peek(key: 1)) cache.save(key: 1, value: 100) // [1] @@ -67,7 +59,7 @@ extension LRUCacheTests { XCTAssertNil(cache.peek(key: 2)) XCTAssertEqual(302, cache.peek(key: 3)) - var node: LRUCache.CacheElement? = cache.head + var node: LruCache.CacheElement? = cache.head var count = 0 while node != nil { count += 1 @@ -76,10 +68,25 @@ extension LRUCacheTests { XCTAssertEqual(maxSize, count - 2) // subtract 2 (head, tail) XCTAssertEqual(cache.map.count, cache.size) } + + func testZeroSize() { + let cache = LruCache(size: 0, timeoutInSecs: 1000) + + XCTAssertNil(cache.lookup(key: 1)) + cache.save(key: 1, value: 100) // [1] + XCTAssertNil(cache.lookup(key: 1)) + } + +} + +// tests below will be skipped in CI (travis/actions) since they can be flaky (depend on timing). +#if DEBUG + +extension LruCacheTests { func testTimeout() { let maxTimeout = 1 - let cache = LRUCache(size: 1000, timeoutInSecs: maxTimeout) + let cache = LruCache(size: 1000, timeoutInSecs: maxTimeout) cache.save(key: 1, value: 100) // [1] cache.save(key: 2, value: 200) // [1, 2] @@ -96,7 +103,7 @@ extension LRUCacheTests { func testAllStale() { let maxTimeout = 1 - let cache = LRUCache(size: 1000, timeoutInSecs: maxTimeout) + let cache = LruCache(size: 1000, timeoutInSecs: maxTimeout) cache.save(key: 1, value: 100) // [1] cache.save(key: 2, value: 200) // [1, 2] @@ -107,13 +114,6 @@ extension LRUCacheTests { XCTAssert(cache.map.isEmpty, "cache should be reset when detected that all items are stale") } - func testZeroSize() { - let cache = LRUCache(size: 0, timeoutInSecs: 1000) - - XCTAssertNil(cache.lookup(key: 1)) - cache.save(key: 1, value: 100) // [1] - XCTAssertNil(cache.lookup(key: 1)) - } } #endif diff --git a/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift similarity index 71% rename from Tests/OptimizelyTests-Common/ODPEventManagerTests.swift rename to Tests/OptimizelyTests-Common/OdpEventManagerTests.swift index c38778e0..c75762fc 100644 --- a/Tests/OptimizelyTests-Common/ODPEventManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift @@ -16,9 +16,9 @@ import XCTest -class ODPEventManagerTests: XCTestCase { - var manager: ODPEventManager! - var odpConfig: OptimizelyODPConfig! +class OdpEventManagerTests: XCTestCase { + var manager: OdpEventManager! + var odpConfig: OdpConfig! var apiManager = MockZaiusApiManager() var options = [OptimizelySegmentOption]() @@ -31,12 +31,13 @@ class ODPEventManagerTests: XCTestCase { "model": "overruled"] override func setUp() { + OTUtils.clearAllEventQueues() OTUtils.createDocumentDirectoryIfNotAvailable() // no valid apiKey, so flush will return immediately - odpConfig = OptimizelyODPConfig() + odpConfig = OdpConfig() - manager = ODPEventManager(sdkKey: "any", + manager = OdpEventManager(sdkKey: "any", odpConfig: odpConfig, apiManager: apiManager) } @@ -87,10 +88,10 @@ class ODPEventManagerTests: XCTestCase { } func testSendEvent_apiKey() { - odpConfig = OptimizelyODPConfig() + odpConfig = OdpConfig() odpConfig.update(apiKey: "valid", apiHost: "host") - manager = ODPEventManager(sdkKey: "any", + manager = OdpEventManager(sdkKey: "any", odpConfig: odpConfig, apiManager: apiManager) manager.sendEvent(type: "t1", @@ -106,7 +107,7 @@ class ODPEventManagerTests: XCTestCase { // MARK: - flush func testFlush_apiKey() { - let event = ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) // apiKey is not ready @@ -121,10 +122,9 @@ class ODPEventManagerTests: XCTestCase { // apiKey is ready odpConfig.update(apiKey: "valid", apiHost: "host") - manager.flush() - sleep(1) + XCTAssertEqual(0, manager.eventQueue.count) } @@ -132,12 +132,14 @@ class ODPEventManagerTests: XCTestCase { func testFlush_batch_1() { let events = [ - ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) ] manager.dispatch(events[0]) odpConfig.update(apiKey: "valid", apiHost: "host") + manager.flush() sleep(1) + XCTAssertEqual(1, apiManager.receivedBatchEvents.count) XCTAssertEqual(1, apiManager.receivedBatchEvents[0].count) validateEvents(events, apiManager.receivedBatchEvents[0]) @@ -145,9 +147,9 @@ class ODPEventManagerTests: XCTestCase { 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: [:]) + 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 { @@ -155,22 +157,26 @@ class ODPEventManagerTests: XCTestCase { } odpConfig.update(apiKey: "valid", apiHost: "host") + manager.flush() sleep(1) + XCTAssertEqual(1, apiManager.receivedBatchEvents.count) XCTAssertEqual(3, apiManager.receivedBatchEvents[0].count) validateEvents(events, apiManager.receivedBatchEvents[0]) } func testFlush_batch_moreThanBatchSize() { - let event = ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) - let events = [ODPEvent](repeating: event, count: 11) + 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") + manager.flush() sleep(1) + XCTAssertEqual(2, apiManager.receivedBatchEvents.count) XCTAssertEqual(10, apiManager.receivedBatchEvents[0].count) XCTAssertEqual(1, apiManager.receivedBatchEvents[1].count) @@ -179,36 +185,86 @@ class ODPEventManagerTests: XCTestCase { func testFlush_emptyQueue() { odpConfig.update(apiKey: "valid", apiHost: "host") + manager.flush() sleep(1) + XCTAssertEqual(0, apiManager.receivedBatchEvents.count) } + + // MARK: - multiple skdKeys + + func testMultipleSdkKeys_doNotInterfere() { + let apiManager1 = MockZaiusApiManager() + let apiManager2 = MockZaiusApiManager() + 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.receivedBatchEvents.count) + XCTAssertEqual(0, apiManager2.receivedBatchEvents.count) + + odpConfig1.update(apiKey: "valid", apiHost: "host") + manager1.flush() + sleep(1) + + XCTAssertEqual(1, apiManager1.receivedBatchEvents.count) + XCTAssertEqual(2, apiManager1.receivedBatchEvents[0].count) + XCTAssertEqual(0, apiManager2.receivedBatchEvents.count) + + odpConfig2.update(apiKey: "valid", apiHost: "host") + manager2.flush() + sleep(1) + + XCTAssertEqual(1, apiManager1.receivedBatchEvents.count) + XCTAssertEqual(2, apiManager1.receivedBatchEvents[0].count) + XCTAssertEqual(1, apiManager2.receivedBatchEvents.count) + XCTAssertEqual(1, apiManager2.receivedBatchEvents[0].count) + } // MARK: - errors func testFlushError_retry() { - let event = ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) - let events = [ODPEvent](repeating: event, count: 2) + 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") + manager.flush() sleep(1) + XCTAssertEqual(3, apiManager.receivedBatchEvents.count, "should be retried 3 times (a batch of 2 events)") XCTAssertEqual(2, manager.eventQueue.count, "the events should remain after giving up") } func testFlushError_noRetry() { - let event = ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) - let events = [ODPEvent](repeating: event, count: 15) + 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") + manager.flush() sleep(1) + XCTAssertEqual(2, apiManager.receivedBatchEvents.count, "should not be retried (only once for each of batch events)") XCTAssertEqual(10, apiManager.receivedBatchEvents[0].count) XCTAssertEqual(5, apiManager.receivedBatchEvents[1].count) @@ -220,14 +276,14 @@ class ODPEventManagerTests: XCTestCase { func testOdpConfig() { odpConfig.update(apiKey: "test-key", apiHost: "test-host") - manager = ODPEventManager(sdkKey: "any", + manager = OdpEventManager(sdkKey: "any", odpConfig: odpConfig, apiManager: apiManager) - let event = ODPEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) manager.dispatch(event) - manager.flush() - + sleep(1) + XCTAssertEqual("test-host", apiManager.receivedApiHost) XCTAssertEqual("test-key", apiManager.receivedApiKey) } @@ -243,19 +299,19 @@ class ODPEventManagerTests: XCTestCase { XCTAssert((data["os_version"] as! String).count > 3) XCTAssert((data["device_type"] as! String).count > 3) - // overruled prop + // overruled ("model") or other custom data if customData.isEmpty { XCTAssert((data["model"] as! String).count > 3) + XCTAssertNil(data["key-1"]) + XCTAssertNil(data["key-2"]) } else { XCTAssert((data["model"] as! String) == "overruled") + XCTAssert((data["key-1"] as! String) == "value-1") + XCTAssert((data["key-2"] as! Double) == 12.5) } - - // other custom data - XCTAssert((data["key-1"] as! String) == "value-1") - XCTAssert((data["key-2"] as! Double) == 12.5) } - func validateEvents(_ lhs: [ODPEvent], _ rhs: [ODPEvent]) { + func validateEvents(_ lhs: [OdpEvent], _ rhs: [OdpEvent]) { XCTAssertEqual(lhs.count, rhs.count) for i in 0.. Void) { receivedApiKey = apiKey receivedApiHost = apiHost diff --git a/Tests/OptimizelyTests-Common/ODPManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift similarity index 51% rename from Tests/OptimizelyTests-Common/ODPManagerTests.swift rename to Tests/OptimizelyTests-Common/OdpManagerTests.swift index 92492922..f1d54294 100644 --- a/Tests/OptimizelyTests-Common/ODPManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -16,16 +16,77 @@ import XCTest -class ODPManagerTests: XCTestCase { - var manager: ODPManager! +class OdpManagerTests: XCTestCase { let sdkKey = "any" - let odpConfig = OptimizelyODPConfig() + let odpConfig = OdpConfig() + 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, + odpConfig: odpConfig) + eventManager = MockOdpEventManager(sdkKey: sdkKey, odpConfig: odpConfig) + manager = OdpManager(sdkKey: sdkKey, + enable: true, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout, + segmentManager: segmentManager, + eventManager: eventManager) + } - func testFetchQualifiedSegments() { - let segmentManager = MockODPSegmentManager(odpConfig: odpConfig) - manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, segmentManager: segmentManager) + override func tearDown() { + OTUtils.clearAllEventQueues() + } + + // MARK: - Configurables + + func testConfigurations_cache() { + let manager = OdpManager(sdkKey: sdkKey, + enable: true, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout) + XCTAssertEqual(manager.segmentManager?.segmentsCache.size, cacheSize) + XCTAssertEqual(manager.segmentManager?.segmentsCache.timeoutInSecs, cacheTimeout) + } + + func testConfigurations_enable() { + let manager = OdpManager(sdkKey: sdkKey, + enable: false, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout) - let vuid = "VUID_123" + XCTAssertTrue(manager.vuid.starts(with: "vuid_"), "vuid should be serverved even when ODP is disabled.") + + let sem = DispatchSemaphore(value: 0) + manager.fetchQualifiedSegments(userId: "user1", segmentsToCheck: [], options: []) { segments, error in + XCTAssertNil(segments) + XCTAssertEqual(error?.reason, OptimizelyError.odpNotEnabled.reason) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + + manager.updateOdpConfig(apiKey: "valid", apiHost: "host") + XCTAssertNil(manager.odpConfig.apiKey) + XCTAssertNil(manager.odpConfig.apiHost) + + // these calls should be dropped gracefully with nil + + manager.identifyUser(userId: "user1") + manager.sendEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) + + XCTAssertNil(manager.eventManager) + XCTAssertNil(manager.segmentManager) + } + + // MARK: - APIs + + func testFetchQualifiedSegments() { + let vuid = "vuid_123" manager.fetchQualifiedSegments(userId: vuid, segmentsToCheck: ["seg-1"], options: [.ignoreCache]) { _, _ in } XCTAssertEqual(segmentManager.receivedUserKey, "vuid") @@ -43,18 +104,10 @@ class ODPManagerTests: XCTestCase { } func testRegisterVUIDCalled() { - let eventManager = MockODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) - manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, eventManager: eventManager) - - // registerVUID is implicitly called on ODPManager init - - XCTAssertEqual(eventManager.receivedVuid, manager.vuid) + XCTAssertEqual(eventManager.receivedVuid, manager.vuid, "registerVUID is implicitly called on OdpManager init") } func testIdentifyUser() { - let eventManager = MockODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) - manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, eventManager: eventManager) - manager.identifyUser(userId: "user-1") XCTAssertEqual(eventManager.receivedVuid, manager.vuid) @@ -62,9 +115,6 @@ class ODPManagerTests: XCTestCase { } func testSendEvent() { - let eventManager = MockODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) - manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, eventManager: eventManager) - // vuid is implicitly added to identifers manager.sendEvent(type: "t1", action: "a1", identifiers: ["id-key1": "id-val-1"], data: ["key1" : "val1"]) @@ -82,43 +132,49 @@ class ODPManagerTests: XCTestCase { XCTAssertEqual(eventManager.receivedIdentifiers, ["vuid": "vuid-fixed", "id-key1": "id-val-1"]) } - func testUpdateODPConfig() { - let eventManager = MockODPEventManager(sdkKey: sdkKey, odpConfig: odpConfig) - manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig, eventManager: eventManager) - - // flush on valid apiKey updated (apiKey/apiHost propagated into submanagers) - - manager.updateODPConfig(apiKey: "key-1", apiHost: "host-1") + func testUpdateOdpConfig_flushCalled() { + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1") + XCTAssertTrue(eventManager.flushCalled) + + manager.updateOdpConfig(apiKey: nil, apiHost: nil) XCTAssertTrue(eventManager.flushCalled) + } + + func testUpdateOdpConfig_odpConfigPropagatedProperly() { + let manager = OdpManager(sdkKey: sdkKey, + enable: true, + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeout) + + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1") - XCTAssertEqual(manager.segmentManager.odpConfig.apiKey, "key-1") - XCTAssertEqual(manager.segmentManager.odpConfig.apiHost, "host-1") - XCTAssertTrue(manager.segmentManager.odpConfig.enabled) - XCTAssertEqual(manager.eventManager.odpConfig.apiKey, "key-1") - XCTAssertEqual(manager.eventManager.odpConfig.apiHost, "host-1") - XCTAssertTrue(manager.eventManager.odpConfig.enabled) + XCTAssertEqual(manager.segmentManager?.odpConfig.apiKey, "key-1") + XCTAssertEqual(manager.segmentManager?.odpConfig.apiHost, "host-1") + XCTAssertEqual(manager.segmentManager?.odpConfig.odpServiceIntegrated, true) + XCTAssertEqual(manager.eventManager?.odpConfig.apiKey, "key-1") + XCTAssertEqual(manager.eventManager?.odpConfig.apiHost, "host-1") + XCTAssertEqual(manager.eventManager?.odpConfig.odpServiceIntegrated, true) // odp disabled with invalid apiKey (apiKey/apiHost propagated into submanagers) - manager.updateODPConfig(apiKey: nil, apiHost: nil) - XCTAssertTrue(eventManager.flushCalled) + manager.updateOdpConfig(apiKey: nil, apiHost: nil) - XCTAssertEqual(manager.segmentManager.odpConfig.apiKey, nil) - XCTAssertEqual(manager.segmentManager.odpConfig.apiHost, nil) - XCTAssertFalse(manager.segmentManager.odpConfig.enabled) - XCTAssertEqual(manager.eventManager.odpConfig.apiKey, nil) - XCTAssertEqual(manager.eventManager.odpConfig.apiHost, nil) - XCTAssertFalse(manager.eventManager.odpConfig.enabled) + XCTAssertEqual(manager.segmentManager?.odpConfig.apiKey, nil) + XCTAssertEqual(manager.segmentManager?.odpConfig.apiHost, nil) + XCTAssertEqual(manager.segmentManager?.odpConfig.odpServiceIntegrated, false) + XCTAssertEqual(manager.eventManager?.odpConfig.apiKey, nil) + XCTAssertEqual(manager.eventManager?.odpConfig.apiHost, nil) + XCTAssertEqual(manager.eventManager?.odpConfig.odpServiceIntegrated, false) } + func testVuid() { - manager = ODPManager(sdkKey: sdkKey, odpConfig: odpConfig) XCTAssertEqual(manager.vuid, manager.vuidManager.vuid) } // MARK: - Helpers - class MockODPEventManager: ODPEventManager { + class MockOdpEventManager: OdpEventManager { var receivedVuid: String! var receivedUserId: String! @@ -150,7 +206,7 @@ class ODPManagerTests: XCTestCase { } } - class MockODPSegmentManager: ODPSegmentManager { + class MockOdpSegmentManager: OdpSegmentManager { var receivedUserKey: String! var receivedUserValue: String! var receivedSegmentsToCheck: [String]! diff --git a/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift similarity index 77% rename from Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift rename to Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift index fddebb9a..dedeca33 100644 --- a/Tests/OptimizelyTests-Common/ODPSegmentManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift @@ -16,9 +16,9 @@ import XCTest -class ODPSegmentManagerTests: XCTestCase { - var manager: ODPSegmentManager! - var odpConfig: OptimizelyODPConfig! +class OdpSegmentManagerTests: XCTestCase { + var manager: OdpSegmentManager! + var odpConfig: OdpConfig! var apiManager = MockZaiusApiManager() var options = [OptimizelySegmentOption]() @@ -27,14 +27,17 @@ class ODPSegmentManagerTests: XCTestCase { var userValue = "test-user" override func setUp() { - odpConfig = OptimizelyODPConfig() - odpConfig.update(apiKey: "valid", apiHost: "host") + odpConfig = OdpConfig() - manager = ODPSegmentManager(odpConfig: odpConfig, + manager = OdpSegmentManager(cacheSize: 10, + cacheTimeoutInSecs: 10, + odpConfig: odpConfig, apiManager: apiManager) } func testFetchSegmentsSuccess_cacheMiss() { + odpConfig.update(apiKey: "valid", apiHost: "host") + setCache(userKey, "123", ["a"]) let sem = DispatchSemaphore(value: 0) @@ -47,9 +50,14 @@ class ODPSegmentManagerTests: XCTestCase { 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") + setCache(userKey, userValue, ["a"]) let sem = DispatchSemaphore(value: 0) @@ -65,8 +73,8 @@ class ODPSegmentManagerTests: XCTestCase { } func testFetchSegmentsError() { - odpConfig.apiKey = "invalid-key" - + odpConfig.update(apiKey: "invalid-key", apiHost: "host") + let sem = DispatchSemaphore(value: 0) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, @@ -79,41 +87,11 @@ class ODPSegmentManagerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - // MARK: - OdpConfig - - func testOdpConfig() { - // default - - manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: apiManager) - manager.fetchQualifiedSegments(userKey: userKey, - userValue: userValue, - segmentsToCheck: [], - options: options) { _, _ in } - - XCTAssertEqual(100, manager.segmentsCache.size) - XCTAssertEqual(600, manager.segmentsCache.timeoutInSecs) - - // custom configuration - - odpConfig.update(apiKey: "test-key", apiHost: "test-host") - - odpConfig = OptimizelyODPConfig(segmentsCacheSize: 3, - segmentsCacheTimeoutInSecs: 30) - manager = ODPSegmentManager(odpConfig: odpConfig, apiManager: apiManager) - manager.fetchQualifiedSegments(userKey: userKey, - userValue: userValue, - segmentsToCheck: [], - options: options) { _, _ in } - - XCTAssertEqual(3, manager.segmentsCache.size) - XCTAssertEqual(39, manager.segmentsCache.timeoutInSecs) - XCTAssertEqual("test-key", apiManager.receivedApiKey) - XCTAssertEqual("test-host", apiManager.receivedApiHost) - } - // MARK: - OptimizelySegmentOption func testOptions_ignoreCache() { + odpConfig.update(apiKey: "valid", apiHost: "host") + setCache(userKey, userValue, ["a"]) options = [.ignoreCache] @@ -131,6 +109,8 @@ class ODPSegmentManagerTests: XCTestCase { } func testOptions_resetCache() { + odpConfig.update(apiKey: "valid", apiHost: "host") + setCache(userKey, userValue, ["a"]) setCache(userKey, "123", ["a"]) setCache(userKey, "456", ["a"]) @@ -187,7 +167,7 @@ class ODPSegmentManagerTests: XCTestCase { DispatchQueue.global().async { if apiKey == "invalid-key" { - completionHandler([], OptimizelyError.fetchSegmentsFailed("403")) + completionHandler(nil, OptimizelyError.fetchSegmentsFailed("403")) } else { completionHandler(["new-customer"], nil) } diff --git a/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift b/Tests/OptimizelyTests-Common/OdpVuidManagerTests.swift similarity index 79% rename from Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift rename to Tests/OptimizelyTests-Common/OdpVuidManagerTests.swift index e59274a4..08871a31 100644 --- a/Tests/OptimizelyTests-Common/ODPVUIDManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpVuidManagerTests.swift @@ -16,29 +16,29 @@ import XCTest -class ODPVUIDManagerTests: XCTestCase { - var manager = ODPVUIDManager() +class OdpVuidManagerTests: XCTestCase { + var manager = OdpVuidManager() func testMakeVuid() { let vuid = manager.makeVuid() - XCTAssertTrue(vuid.starts(with: "VUID_")) + XCTAssertTrue(vuid.starts(with: "vuid_")) XCTAssertTrue(vuid.count > 20) } func testIsVuid() { - XCTAssertTrue(manager.isVuid(visitorId: "VUID_123")) - XCTAssertFalse(manager.isVuid(visitorId: "VUID-123")) + XCTAssertTrue(manager.isVuid(visitorId: "vuid_123")) + XCTAssertFalse(manager.isVuid(visitorId: "vuid-123")) XCTAssertFalse(manager.isVuid(visitorId: "123")) } func testAutoSaveAndLoad() { UserDefaults.standard.removeObject(forKey: "optimizely-odp") - manager = ODPVUIDManager() + manager = OdpVuidManager() let vuid1 = manager.vuid - manager = ODPVUIDManager() + manager = OdpVuidManager() let vuid2 = manager.vuid XCTAssertTrue(vuid1 == vuid2) @@ -47,7 +47,7 @@ class ODPVUIDManagerTests: XCTestCase { UserDefaults.standard.removeObject(forKey: "optimizely-odp") - manager = ODPVUIDManager() + manager = OdpVuidManager() let vuid3 = manager.vuid XCTAssertTrue(vuid1 != vuid3) diff --git a/Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift b/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift similarity index 100% rename from Tests/OptimizelyTests-Common/ODPZaiusGraphQLApiManagerTests.swift rename to Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift diff --git a/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift b/Tests/OptimizelyTests-Common/OdpZaiusRestApiManagerTests.swift similarity index 88% rename from Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift rename to Tests/OptimizelyTests-Common/OdpZaiusRestApiManagerTests.swift index bcd373c7..17dfba8b 100644 --- a/Tests/OptimizelyTests-Common/ODPZaiusRestApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpZaiusRestApiManagerTests.swift @@ -22,15 +22,15 @@ class ZaiusRestApiManagerTests: XCTestCase { 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: ["key-1": "value-1"]), - ODPEvent(type: "t2", action: "a2", identifiers: ["id-key-2": "id-value-2"], data: ["key-2": "value-2"]) + let events: [OdpEvent] = [ + OdpEvent(type: "t1", action: "a1", identifiers: ["id-key-1": "id-value-1"], data: ["key-1": "value-1"]), + OdpEvent(type: "t2", action: "a2", identifiers: ["id-key-2": "id-value-2"], data: ["key-2": "value-2"]) ] - func testSendODPEvents_validRequest() { + func testSendOdpEvents_validRequest() { let session = MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.successResponseData) let api = MockZaiusApiManager(session) - api.sendODPEvents(apiKey: apiKey, apiHost: apiHost, events: events) { _ in } + api.sendOdpEvents(apiKey: apiKey, apiHost: apiHost, events: events) { _ in } let request = session.receivedApiRequest! @@ -47,22 +47,22 @@ class ZaiusRestApiManagerTests: XCTestCase { } } - func testSendODPEvents_success() { + func testSendOdpEvents_success() { let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.successResponseData)) let sem = DispatchSemaphore(value: 0) - api.sendODPEvents(apiKey: apiKey, apiHost: apiHost, events: events) { error in + api.sendOdpEvents(apiKey: apiKey, apiHost: apiHost, events: events) { error in XCTAssertNil(error) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - func testSendODPEvents_networkError_retry() { + func testSendOdpEvents_networkError_retry() { let api = MockZaiusApiManager(MockZaiusUrlSession(withError: true)) let sem = DispatchSemaphore(value: 0) - api.sendODPEvents(apiKey: apiKey, apiHost: apiHost, events: events) { error in + api.sendOdpEvents(apiKey: apiKey, apiHost: apiHost, events: events) { error in if case .odpEventFailed(_, let canRetry) = error { XCTAssertTrue(canRetry) } else { @@ -73,11 +73,11 @@ class ZaiusRestApiManagerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - func testSendODPEvents_400_noRetry() { + func testSendOdpEvents_400_noRetry() { let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 400, responseData: MockZaiusUrlSession.failureResponseData)) let sem = DispatchSemaphore(value: 0) - api.sendODPEvents(apiKey: apiKey, apiHost: apiHost, events: events) { error in + api.sendOdpEvents(apiKey: apiKey, apiHost: apiHost, events: events) { error in if case .odpEventFailed(_, let canRetry) = error { XCTAssertFalse(canRetry) } else { @@ -88,11 +88,11 @@ class ZaiusRestApiManagerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - func testSendODPEvents_500_retry() { + func testSendOdpEvents_500_retry() { let api = MockZaiusApiManager(MockZaiusUrlSession(statusCode: 500, responseData: "server error")) let sem = DispatchSemaphore(value: 0) - api.sendODPEvents(apiKey: apiKey, apiHost: apiHost, events: events) { error in + api.sendOdpEvents(apiKey: apiKey, apiHost: apiHost, events: events) { error in if case .odpEventFailed(_, let canRetry) = error { XCTAssertTrue(canRetry) } else { @@ -116,7 +116,7 @@ class ZaiusRestApiManagerTests: XCTestCase { return mockUrlSession } - override func sendODPEvents(apiKey: String, apiHost: String, events: [ODPEvent], completionHandler: @escaping (OptimizelyError?) -> Void) { + override func sendOdpEvents(apiKey: String, apiHost: String, events: [OdpEvent], completionHandler: @escaping (OptimizelyError?) -> Void) { } } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift similarity index 50% rename from Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift rename to Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index 18fef174..d1effa37 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -16,25 +16,24 @@ import XCTest -class OptimizelyUserContextTests_Segments: XCTestCase { +class OptimizelyUserContextTests_ODP: XCTestCase { var optimizely: OptimizelyClient! - var odpConfig: OptimizelyODPConfig! var user: OptimizelyUserContext! let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + var odpManager: MockOdpManager! let kUserId = "tester" - let kApiKey = "any-key" - let kApiHost = "any-host" let kUserKey = "custom_id" let kUserValue = "custom_id_value" - + let sdkKey = OTUtils.randomSdkKey + override func setUp() { - odpConfig = OptimizelyODPConfig(segmentsCacheSize: 100, - segmentsCacheTimeoutInSecs: 100, - apiHost: kApiHost, - apiKey: kApiKey) - optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, odpConfig: odpConfig) + odpManager = MockOdpManager(sdkKey: sdkKey, enable: true, cacheSize: 10, cacheTimeoutInSecs: 10) + + optimizely = OptimizelyClient(sdkKey: sdkKey) + optimizely.odpManager = odpManager + user = optimizely.createUserContext(userId: kUserId) } @@ -54,6 +53,8 @@ class OptimizelyUserContextTests_Segments: XCTestCase { // MARK: - Success func testFetchQualifiedSegments_successDefaultUser() { + try? optimizely.start(datafile: datafile) + let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments { segments, error in XCTAssertNil(error) @@ -82,8 +83,8 @@ class OptimizelyUserContextTests_Segments: XCTestCase { func testFetchQualifiedSegments_fetchFailed() { let sem = DispatchSemaphore(value: 0) - odpConfig.apiKey = "invalid-key" - + // ODP apiKey is not available + user.fetchQualifiedSegments { segments, error in XCTAssertNotNil(error) XCTAssertNil(segments) @@ -96,7 +97,6 @@ class OptimizelyUserContextTests_Segments: XCTestCase { // MARK: - SegmentsToCheck func testFetchQualifiedSegments_segmentsToCheck_validAfterStart() { - let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! try? optimizely.start(datafile: datafile) let sem = DispatchSemaphore(value: 0) @@ -105,181 +105,93 @@ class OptimizelyUserContextTests_Segments: XCTestCase { } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!)) + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(odpManager.segmentsToCheck!)) } - func testFetchQualifiedSegments_segmentsToCheck_segmentsNotUsed() { + func testFetchQualifiedSegments_segmentsNotUsed() { let datafile = OTUtils.loadJSONDatafile("decide_datafile")! try? optimizely.start(datafile: datafile) let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, apiHost: kApiHost) { _, _ in + user.fetchQualifiedSegments { segments, error in + XCTAssertNil(error) + XCTAssertEqual(segments, []) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - - XCTAssertNil(segmentHandler.segmentsToCheck, "empty segmentsToCheck case should be filtered out before calling segmentHandler") - XCTAssertEqual([], user.qualifiedSegments) } - // MARK: - Customisze AudienceSegmentHandler + // MARK: - Customisze OdpManager - func testCustomizeODPManager() { + func testCustomizeOdpManager() { + let sdkSettings = OptimizelySdkSettings(segmentsCacheSize: 12, segmentsCacheTimeoutInSecs: 345) let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, - periodicDownloadInterval: 60, - segmentsCacheSize: 12, - segmentsCacheTimeout: 123) + settings: sdkSettings) - XCTAssertEqual(12, optimizely.odpManager.segmentsCache.size) - XCTAssertEqual(123, optimizely.odpManager.segmentsCache.timeoutInSecs) + XCTAssertEqual(12, optimizely.odpManager.segmentManager?.segmentsCache.size) + XCTAssertEqual(345, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) } } // MARK: - Optional parameters -extension OptimizelyUserContextTests_Segments { +extension OptimizelyUserContextTests_ODP { func testFetchQualifiedSegments_parameters() { - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey, - apiHost: kApiHost, - userKey: kUserKey, - userValue: kUserValue, - options: [.ignoreCache]) { _, _ in - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - - XCTAssertEqual(kApiKey, segmentHandler.apiKey) - XCTAssertEqual(kApiHost, segmentHandler.apiHost) - XCTAssertEqual(kUserKey, segmentHandler.userKey) - XCTAssertEqual(kUserValue, segmentHandler.userValue) - XCTAssertNil(segmentHandler.segmentsToCheck) - XCTAssertEqual([.ignoreCache], segmentHandler.options) - } - - func testFetchQualifiedSegments_defaults_configReady() { - try! optimizely.start(datafile: datafile) + try? optimizely.start(datafile: datafile) let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments() { segments, error in + user.fetchQualifiedSegments(options: [.ignoreCache]) { segments, error in XCTAssertNil(error) XCTAssertEqual(segments, ["segment-1"]) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - - XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", optimizely.config?.publicKeyForODP, "apiKey from datafile should be used as a default") - XCTAssertEqual("https://api.zaius.com", optimizely.config?.hostForODP, "apiHost from datafile should be used as a default") - XCTAssertEqual("fs_user_id", segmentHandler.userKey, "the reserved user-key should be used as a default") - XCTAssertEqual(kUserId, segmentHandler.userValue, "userId should be used as a default") - XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segmentHandler.segmentsToCheck!), "segmentsToCheck should be all-in-project by default") - XCTAssertEqual([], segmentHandler.options) - } - - func testFetchQualifiedSegments_missingApiKey_configNotReady() { - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments() { segments, error in - if let error = error, case OptimizelyError.fetchSegmentsFailed(let hint) = error { - XCTAssertEqual("apiKey not defined", hint) - } else { - XCTFail() - } - XCTAssertNil(segments) - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - } - func testFetchQualifiedSegments_missingApiHost_configNotReady() { - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: kApiKey) { segments, error in - if let error = error, case OptimizelyError.fetchSegmentsFailed(let hint) = error { - XCTAssertEqual("apiHost not defined", hint) - } else { - XCTFail() - } - XCTAssertNil(segments) - 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.segmentsToCheck!), "segmentsToCheck should be all-in-project by default") + XCTAssertEqual([.ignoreCache], odpManager.options) } - func testFetchQualifiedSegments_defaults_configReady_missingIntegration() { - let datafile = OTUtils.loadJSONDatafile("decide_datafile")! // no integration in this datafile - try! optimizely.start(datafile: datafile) + func testFetchQualifiedSegments_configReady() { + XCTAssertNil(odpManager.odpConfig.apiKey) + XCTAssertNil(odpManager.odpConfig.apiHost) - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments() { segments, error in - if let error = error, case OptimizelyError.fetchSegmentsFailed(let hint) = error { - XCTAssertEqual("apiKey not defined", hint) - } else { - XCTFail() - } - XCTAssertNil(segments) - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - } - -} + try? optimizely.start(datafile: "invalid") -// MARK: - MockODPManager + XCTAssertNil(odpManager.odpConfig.apiKey) + XCTAssertNil(odpManager.odpConfig.apiHost) + + try! optimizely.start(datafile: datafile) -class MockODPManager: ODPManager { - var segmentsToCheck: [String]! - - override func fetchQualifiedSegments(userId: String, - segmentsToCheck: [String], - options: [OptimizelySegmentOption], - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - self.segmentsToCheck = segmentsToCheck + XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", odpManager.odpConfig.apiKey) + XCTAssertEqual("https://api.zaius.com", odpManager.odpConfig.apiHost) - DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - if self.odpConfig.apiKey == "invalid-key" { - completionHandler(nil, OptimizelyError.generic) - } else { - let sampleSegments = ["segment-1"] - completionHandler(sampleSegments, nil) - } - } + 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 real ODP server -// TODO: this test can be flaky. replace it with a good test account or remove it later. -extension OptimizelyUserContextTests_Segments { - var testODPApiHost: String { return "https://api.zaius.com" } - var testODPApiKeyForAudienceSegments: String { return "W4WzcEs-ABgXorzY7h1LCQ" } +// 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 "vuid" } - var testODPUserValue: String { return "00TEST00VUID00FULLSTACK" } - var testODPUserId: String { return "tester-101"} + var testOdpUserKey: String { return "fs_user_id" } + var testOdpUserId: String { return "tester-101"} - func testLiveODPGraphQL() { + func testLiveOdpGraphQL() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) - let user = optimizely.createUserContext(userId: testODPUserId) + let user = optimizely.createUserContext(userId: testOdpUserId) - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: testODPApiKeyForAudienceSegments, - apiHost: testODPApiHost, - userKey: testODPUserKey, - userValue: testODPUserValue) { 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() { - 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) @@ -289,15 +201,12 @@ extension OptimizelyUserContextTests_Segments { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } - func testLiveODPGraphQL_noDatafile() { + func testLiveOdpGraphQL_noDatafile() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - let user = optimizely.createUserContext(userId: testODPUserId) + let user = optimizely.createUserContext(userId: testOdpUserId) let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments(apiKey: testODPApiKeyForAudienceSegments, - apiHost: testODPApiHost, - userKey: testODPUserKey, - userValue: testODPUserValue) { segments, error in + user.fetchQualifiedSegments { segments, error in XCTAssertNil(error) XCTAssert(segments!.contains("has_email"), "segmentsToCheck are not passed to ODP, so fetching all segments.") sem.signal() @@ -305,7 +214,7 @@ extension OptimizelyUserContextTests_Segments { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } - func testLiveODPGraphQL_defaultParameters_userNotRegistered() { + func testLiveOdpGraphQL_defaultParameters_userNotRegistered() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) let user = optimizely.createUserContext(userId: "not-registered-user") @@ -324,3 +233,36 @@ extension OptimizelyUserContextTests_Segments { } } + +#endif + +// MARK: - MockOdpManager + +class MockOdpManager: OdpManager { + var userId: String? + var segmentsToCheck: [String]! + var options: [OptimizelySegmentOption]! + + var apiKey: String? + var apiHost: String? + + override func fetchQualifiedSegments(userId: String, + segmentsToCheck: [String], + options: [OptimizelySegmentOption], + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { + self.userId = userId + self.segmentsToCheck = segmentsToCheck + self.options = options + + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + if self.odpConfig.apiKey == nil { + completionHandler(nil, OptimizelyError.generic) + } else { + let sampleSegments = ["segment-1"] + completionHandler(sampleSegments, nil) + } + } + } + +} + diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_Decide.swift similarity index 98% rename from Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift rename to Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_Decide.swift index 6f7bdd88..82b4e898 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Segments_Decide.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP_Decide.swift @@ -16,7 +16,7 @@ import XCTest -class OptimizelyUserContextTests_Segments_Decide: XCTestCase { +class OptimizelyUserContextTests_ODP_Decide: XCTestCase { var optimizely: OptimizelyClient! var user: OptimizelyUserContext! diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index 34cd2cd6..5f989f84 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -88,7 +88,7 @@ class OTUtils { } static func compareDictionaries(_ d1: [String: Any], _ d2: [String: Any]) -> Bool { - if #available(tvOS 11.0, *) { + 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 @@ -233,6 +233,7 @@ class OTUtils { } static func clearAllEventQueues() { + // clear all FS + ODP event queues removeAllFiles(including: "OPTEvent", in: .documentDirectory) removeAllFiles(including: "OPTEvent", in: .cachesDirectory) } From 73062fae50e1025b6b7c2723978d8ed3b1dbbab4 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 8 Jul 2022 14:28:49 -0700 Subject: [PATCH 54/84] more odp tests --- Sources/ODP/OdpConfig.swift | 5 +- Sources/ODP/OdpVuidManager.swift | 2 +- Sources/ODP/ZaiusGraphQLApiManager.swift | 27 ++- Sources/ODP/ZaiusRestApiManager.swift | 7 +- .../OdpZaiusGraphQLApiManagerTests.swift | 190 ++++++++++++------ .../OdpZaiusRestApiManagerTests.swift | 74 +++++-- .../OptimizelyUserContextTests_ODP.swift | 23 +-- 7 files changed, 220 insertions(+), 108 deletions(-) diff --git a/Sources/ODP/OdpConfig.swift b/Sources/ODP/OdpConfig.swift index 6924a5b9..094d8eff 100644 --- a/Sources/ODP/OdpConfig.swift +++ b/Sources/ODP/OdpConfig.swift @@ -22,13 +22,14 @@ class OdpConfig { /// The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile. private var _apiKey: String? /// assumed integrated by default (set to false when datafile has no ODP key/host settings) - private var _odpServiceIntegrated = true + private var _odpServiceIntegrated: Bool let queue = DispatchQueue(label: "odpConfig") - init(apiKey: String? = nil, apiHost: String? = nil) { + init(apiKey: String? = nil, apiHost: String? = nil, odpServiceIntegrated: Bool = true) { self._apiKey = apiKey self._apiHost = apiHost + self._odpServiceIntegrated = odpServiceIntegrated } func update(apiKey: String?, apiHost: String?) { diff --git a/Sources/ODP/OdpVuidManager.swift b/Sources/ODP/OdpVuidManager.swift index 52abb65f..46787af5 100644 --- a/Sources/ODP/OdpVuidManager.swift +++ b/Sources/ODP/OdpVuidManager.swift @@ -32,7 +32,7 @@ class OdpVuidManager { } func isVuid(visitorId: String) -> Bool { - return visitorId.starts(with: "vuid") + return visitorId.starts(with: "vuid_") } } diff --git a/Sources/ODP/ZaiusGraphQLApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift index f096e8e4..fc29dd67 100644 --- a/Sources/ODP/ZaiusGraphQLApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -130,45 +130,52 @@ class ZaiusGraphQLApiManager { defer { session.finishTasksAndInvalidate() } let task = session.dataTask(with: urlRequest) { data, response, error in - guard error != nil, let data = data, let response = response as? HTTPURLResponse else { + 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)" } - completionHandler(nil, .fetchSegmentsFailed("network error")) + returnError = .fetchSegmentsFailed("network error") return } let status = response.statusCode guard status < 400 else { - completionHandler(nil, .fetchSegmentsFailed("\(status)")) + returnError = .fetchSegmentsFailed("\(status)") return } guard let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - completionHandler(nil, .fetchSegmentsFailed("decode error")) + 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: "extension.classification") { + if let odpError = odpErrors.first, let errorClass: String = odpError.extractComponent(keyPath: "extensions.classification") { if errorClass == "InvalidIdentifierException" { - completionHandler(nil, .invalidSegmentIdentifier) + returnError = .invalidSegmentIdentifier } else { - completionHandler(nil, .fetchSegmentsFailed(errorClass)) + returnError = .fetchSegmentsFailed(errorClass) } return } } guard let audDict: [[String: Any]] = dict.extractComponent(keyPath: "data.customer.audiences.edges") else { - completionHandler(nil, .fetchSegmentsFailed("decode error")) + returnError = .fetchSegmentsFailed("decode error") return } let audiences = audDict.compactMap { OdpAudience($0["node"] as? [String: Any]) } - let segments = audiences.filter { $0.isQualified }.map { $0.name } - completionHandler(segments, nil) + returnSegments = audiences.filter { $0.isQualified }.map { $0.name } } task.resume() diff --git a/Sources/ODP/ZaiusRestApiManager.swift b/Sources/ODP/ZaiusRestApiManager.swift index 9aceda43..b027afe8 100644 --- a/Sources/ODP/ZaiusRestApiManager.swift +++ b/Sources/ODP/ZaiusRestApiManager.swift @@ -80,11 +80,16 @@ class ZaiusRestApiManager { 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 = "\(response.statusCode)" + errMessage = dataStr ?? "\(response.statusCode)" canRetry = false // no retry (client error) default: errMessage = "\(response.statusCode)" diff --git a/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift b/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift index 4a4171b1..266107f9 100644 --- a/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift @@ -112,7 +112,7 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { userKey: userKey, userValue: userValue, segmentsToCheck: []) { segments, error in - if case .invalidSegmentIdentifier = error { + if case .fetchSegmentsFailed("TestExceptionClass") = error { XCTAssert(true) } else { XCTFail() @@ -132,7 +132,7 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { userKey: userKey, userValue: userValue, segmentsToCheck: []) { segments, error in - if case .fetchSegmentsFailed("TestExceptionClass") = error { + if case .fetchSegmentsFailed("decode error") = error { XCTAssert(true) } else { XCTFail() @@ -222,7 +222,67 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { XCTAssertNil(dict.extractComponent(keyPath: "d")) } - // MARK: - MockZaiusApiManager + +} + +// 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 ZaiusGraphQLApiManagerTests { + + var odpApiKey: String { return "W4WzcEs-ABgXorzY7h1LCQ" } + var odpApiHost: String { return "https://api.zaius.com" } + var odpValidUserId: String { return "tester-101"} + + func testLiveOdpGraphQL() { + let manager = ZaiusGraphQLApiManager() + + 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 = ZaiusGraphQLApiManager() + + let sem = DispatchSemaphore(value: 0) + manager.fetchSegments(apiKey: odpApiKey, + apiHost: odpApiHost, + userKey: "fs_user_id", + userValue: "not-registered-user", + 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: - MockZaiusApiManager + +extension ZaiusGraphQLApiManagerTests { class MockZaiusApiManager: ZaiusGraphQLApiManager { let mockUrlSession: URLSession @@ -287,91 +347,89 @@ class ZaiusGraphQLApiManagerTests: XCTestCase { // 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" - } + { + "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": [] - } + { + "data": { + "customer": { + "audiences": { + "edges": [] } } } - """ + } + """ static let invalidIdentifierResponseData: String = """ + { + "errors": [ { - "errors": [ + "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd", + "locations": [ { - "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" - } + "line": 2, + "column": 3 } ], - "data": { - "customer": null + "path": [ + "customer" + ], + "extensions": { + "classification": "InvalidIdentifierException" } } - """ + ], + "data": { + "customer": null + } + } + """ static let otherExceptionResponseData: String = """ + { + "errors": [ { - "errors": [ - { - "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd", - "extensions": { - "classification": "TestExceptionClass" - } - } - ], - "data": { - "customer": null + "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd", + "extensions": { + "classification": "TestExceptionClass" } } - """ - - static let badResponseData: String = """ - { - "data": {} - } - """ + ], + "data": { + "customer": null + } + } + """ + static let badResponseData: String = """ + { + "data": {} + } + """ } - } diff --git a/Tests/OptimizelyTests-Common/OdpZaiusRestApiManagerTests.swift b/Tests/OptimizelyTests-Common/OdpZaiusRestApiManagerTests.swift index 17dfba8b..4a8798b9 100644 --- a/Tests/OptimizelyTests-Common/OdpZaiusRestApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpZaiusRestApiManagerTests.swift @@ -30,7 +30,12 @@ class ZaiusRestApiManagerTests: XCTestCase { func testSendOdpEvents_validRequest() { let session = MockZaiusUrlSession(statusCode: 200, responseData: MockZaiusUrlSession.successResponseData) let api = MockZaiusApiManager(session) - api.sendOdpEvents(apiKey: apiKey, apiHost: apiHost, events: events) { _ in } + + 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! @@ -103,7 +108,56 @@ class ZaiusRestApiManagerTests: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) } - // MARK: - MockZaiusApiManager +} + +// 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 ZaiusRestApiManagerTests { + + var odpApiKey: String { return "W4WzcEs-ABgXorzY7h1LCQ" } + var odpApiHost: String { return "https://api.zaius.com" } + var odpValidUserId: String { return "tester-101"} + + func testLiveOdpRest() { + let manager = ZaiusRestApiManager() + + let sem = DispatchSemaphore(value: 0) + manager.sendOdpEvents(apiKey: odpApiKey, + apiHost: odpApiHost, + events: [OdpEvent(type: "t1", + action: "a1", + identifiers: ["vuid": "idv1"], + data: [:])]) { error in + XCTAssertNil(error) + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + } + + func testLiveOdpRest_error() { + let manager = ZaiusRestApiManager() + + 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: - MockZaiusApiManager + +extension ZaiusRestApiManagerTests { class MockZaiusApiManager: ZaiusRestApiManager { let mockUrlSession: URLSession @@ -115,13 +169,8 @@ class ZaiusRestApiManagerTests: XCTestCase { override func getSession() -> URLSession { return mockUrlSession } - - override func sendOdpEvents(apiKey: String, apiHost: String, events: [OdpEvent], completionHandler: @escaping (OptimizelyError?) -> Void) { - } } - // MARK: - MockZaiusUrlSession - class MockZaiusUrlSession: URLSession { static var validSessions = 0 var statusCode: Int @@ -172,12 +221,11 @@ class ZaiusRestApiManagerTests: XCTestCase { // MARK: - Utils static let successResponseData: String = """ - {"title":"Accepted","status":202,"timestamp":"2022-07-01T16:04:06.786Z"} - """ - + {"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"}]}} - """ + {"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/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index d1effa37..63f75254 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -177,7 +177,7 @@ extension OptimizelyUserContextTests_ODP { } -// MARK: - Tests with real ODP server +// MARK: - Tests with live ODP server // tests below will be skipped in CI (travis/actions) since they use the live ODP server. #if DEBUG @@ -201,19 +201,6 @@ extension OptimizelyUserContextTests_ODP { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } - func testLiveOdpGraphQL_noDatafile() { - let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - let user = optimizely.createUserContext(userId: testOdpUserId) - - let sem = DispatchSemaphore(value: 0) - user.fetchQualifiedSegments { segments, error in - XCTAssertNil(error) - XCTAssert(segments!.contains("has_email"), "segmentsToCheck are not passed to ODP, so fetching all segments.") - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) - } - func testLiveOdpGraphQL_defaultParameters_userNotRegistered() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) @@ -221,7 +208,13 @@ extension OptimizelyUserContextTests_ODP { let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments { segments, error in - if case .fetchSegmentsFailed("segments not in json") = error { + 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() From 273e282321eef08cc92bdd62deb714d0e8a4f016 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 8 Jul 2022 16:19:40 -0700 Subject: [PATCH 55/84] add more odp tests --- .../xcschemes/DemoSwiftwatchOS.xcscheme | 25 ++- .../OptimizelyClient+Decide.swift | 1 - .../OptimizelyUserContext.swift | 14 +- Sources/Optimizely/OptimizelyClient.swift | 2 +- .../OptimizelyClientTests_ODP.swift | 167 ++++++++++++++++-- .../OdpManagerTests.swift | 6 +- .../OptimizelyUserContextTests_ODP.swift | 24 +-- 7 files changed, 193 insertions(+), 46 deletions(-) diff --git a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme index 1ebde8af..7aa2b41d 100644 --- a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme +++ b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme @@ -54,8 +54,10 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - + + + + + diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index 47d4191e..acae720c 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -36,7 +36,6 @@ extension OptimizelyClient { /// - 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 { - let vuid = odpManager.vuidManager.vuid return OptimizelyUserContext(optimizely: self, userId: vuid, attributes: attributes) } diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index b546826a..139556d0 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -71,21 +71,9 @@ public class OptimizelyUserContext { userId: String, attributes: [String: Any?]? = nil) { self.init(optimizely: optimizely, userId: userId, attributes: attributes ?? [:]) - self.optimizely?.registerUserToOdp(userId: userId) + self.optimizely?.identifyUserToOdp(userId: userId) } - /// OptimizelyUserContext init for vuid-based decision - /// - /// When a userId is not provided, a user context will be created with the device vuid as a default user id. - /// - /// - Parameters: - /// - optimizely: An instance of OptimizelyClient to be used for decisions. - /// - attributes: A map of attribute names to current user attribute values. - public convenience init(optimizely: OptimizelyClient, - attributes: [String: Any?]? = nil) { - self.init(optimizely: optimizely, userId: optimizely.vuid, attributes: attributes ?? [:]) - } - init(optimizely: OptimizelyClient, userId: String, attributes: [String: Any?]) { diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 0c36ea75..6acf05d6 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -949,7 +949,7 @@ extension OptimizelyClient { return odpManager.vuid } - func registerUserToOdp(userId: String) { + func identifyUserToOdp(userId: String) { odpManager.identifyUser(userId: userId) } diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift index fbb31b9d..eaa46694 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift @@ -27,19 +27,164 @@ class OptimizelyClientTests_ODP: XCTestCase { optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) } - - func testOdp() { - XCTFail() + + // MARK: - public APIs + + func testConfigurableSettings_default() { + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + + XCTAssertEqual(100, optimizely.odpManager.segmentManager?.segmentsCache.size) + 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.size) + XCTAssertEqual(345, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) + + sdkSettings = OptimizelySdkSettings(enableOdp: false) + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) + XCTAssertEqual(false, optimizely.odpManager.enabled) + } + + func testSendOdpEvent() { + let odpManager = MockOdpManager(sdkKey: "any", enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + optimizely.odpManager = odpManager + + optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k2": "v2"]) + + XCTAssertEqual("t1", odpManager.eventType) + XCTAssertEqual("a1", odpManager.eventAction) + XCTAssertEqual(["k1": "v1"], odpManager.eventIdentifiers) + XCTAssertEqual(["k2": "v2"], odpManager.eventData as! [String: String]) + + // default event props + + optimizely.sendOdpEvent(action: "a2") + + XCTAssertEqual("fullstack", odpManager.eventType) + XCTAssertEqual("a2", odpManager.eventAction) + XCTAssertEqual([:], odpManager.eventIdentifiers) + XCTAssertEqual([:], odpManager.eventData as! [String: String]) } + func testVuid() { + XCTAssert(optimizely.vuid.starts(with: "vuid_")) + } + + // MARK: - OdpConfig Update + + func testUpdateOpdConfigCalled_synchronous_success() { + let odpManager = MockOdpManager(sdkKey: "any", enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + optimizely.odpManager = odpManager + + XCTAssertNil(odpManager.apiKey) + XCTAssertNil(odpManager.apiHost) + + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + try? optimizely.start(datafile: datafile) + + XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", odpManager.apiKey, "updateOdpConfig should be called when datafile parsed ok") + XCTAssertEqual("https://api.zaius.com", odpManager.apiHost) + } + + func testUpdateOpdConfigCalled_synchronous_failure() { + let odpManager = MockOdpManager(sdkKey: "any", enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + optimizely.odpManager = odpManager + + XCTAssertNil(odpManager.apiKey) + XCTAssertNil(odpManager.apiHost) + + let datafile = OTUtils.loadJSONDatafile("unsupported_datafile")! + try? optimizely.start(datafile: datafile) + + XCTAssertNil(odpManager.apiKey, "updateOdpConfig should not be called when datafile parse failed") + XCTAssertNil(odpManager.apiHost) + } + + func testUpdateOpdConfigCalled_asynchronous_success() { + let sdkKey = "valid" + let optimizely = OptimizelyClient(sdkKey: sdkKey, datafileHandler: MockDatafileHandler()) + let odpManager = MockOdpManager(sdkKey: sdkKey, enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + optimizely.odpManager = odpManager + + XCTAssertNil(odpManager.apiKey) + XCTAssertNil(odpManager.apiHost) + + let sem = DispatchSemaphore(value: 0) + optimizely.start { result in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", odpManager.apiKey, "updateOdpConfig should be called when datafile fetched") + XCTAssertEqual("https://api.zaius.com", odpManager.apiHost) + } + + func testUpdateOpdConfigCalled_asynchronous_failure() { + let sdkKey = "invalid" + let optimizely = OptimizelyClient(sdkKey: sdkKey, datafileHandler: MockDatafileHandler()) + let odpManager = MockOdpManager(sdkKey: sdkKey, enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + optimizely.odpManager = odpManager + + XCTAssertNil(odpManager.apiKey) + XCTAssertNil(odpManager.apiHost) + + let sem = DispatchSemaphore(value: 0) + optimizely.start { result in + sem.signal() + } + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) + + XCTAssertNil(odpManager.apiKey, "updateOdpConfig should not be called on datafile fetch failed") + XCTAssertNil(odpManager.apiHost) + } + + } +// 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? -//extension OptimizelyClientTests_ODP { -// -// class MockOdpManager: OdpManager { -// -// override init( -// } -// -//} + 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?) { + self.apiKey = apiKey + self.apiHost = apiHost + } + } + + class MockDatafileHandler: DefaultDatafileHandler { + + override func downloadDatafile(sdkKey: String, + returnCacheIfNoChange: Bool, + resourceTimeoutInterval: Double?, + completionHandler: @escaping DatafileDownloadCompletionHandler) { + if sdkKey == "valid" { + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + completionHandler(.success(datafile)) + } else { + completionHandler(.failure(.generic)) + } + } + + } + +} diff --git a/Tests/OptimizelyTests-Common/OdpManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift index f1d54294..a1d7b5f2 100644 --- a/Tests/OptimizelyTests-Common/OdpManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -68,7 +68,7 @@ class OdpManagerTests: XCTestCase { XCTAssertEqual(error?.reason, OptimizelyError.odpNotEnabled.reason) sem.signal() } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) + XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) manager.updateOdpConfig(apiKey: "valid", apiHost: "host") XCTAssertNil(manager.odpConfig.apiKey) @@ -103,14 +103,14 @@ class OdpManagerTests: XCTestCase { XCTAssertEqual(segmentManager.receivedOptions, []) } - func testRegisterVUIDCalled() { + func testRegisterVUIDCalledAutomatically() { XCTAssertEqual(eventManager.receivedVuid, manager.vuid, "registerVUID is implicitly called on OdpManager init") } func testIdentifyUser() { manager.identifyUser(userId: "user-1") - XCTAssertEqual(eventManager.receivedVuid, manager.vuid) + XCTAssertEqual(eventManager.receivedVuid, manager.vuid, "vuid should be added implicitly") XCTAssertEqual(eventManager.receivedUserId, "user-1") } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index 63f75254..a0881b60 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -37,6 +37,13 @@ class OptimizelyUserContextTests_ODP: XCTestCase { user = optimizely.createUserContext(userId: kUserId) } + // MARK: - identify + + func testIdentifyCalledAutomatically() { + XCTAssertEqual(true, odpManager.identifyCalled, "identifyUser is implicitly called on UserContext init") + XCTAssertEqual(kUserId, odpManager.userId) + } + // MARK: - isQualifiedFor func testIsQualifiedFor() { @@ -121,17 +128,6 @@ class OptimizelyUserContextTests_ODP: XCTestCase { XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) } - // MARK: - Customisze OdpManager - - func testCustomizeOdpManager() { - let sdkSettings = OptimizelySdkSettings(segmentsCacheSize: 12, segmentsCacheTimeoutInSecs: 345) - let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, - settings: sdkSettings) - - XCTAssertEqual(12, optimizely.odpManager.segmentManager?.segmentsCache.size) - XCTAssertEqual(345, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) - } - } // MARK: - Optional parameters @@ -235,6 +231,7 @@ class MockOdpManager: OdpManager { var userId: String? var segmentsToCheck: [String]! var options: [OptimizelySegmentOption]! + var identifyCalled = false var apiKey: String? var apiHost: String? @@ -257,5 +254,10 @@ class MockOdpManager: OdpManager { } } + override func identifyUser(userId: String) { + self.userId = userId + self.identifyCalled = true + } + } From 7974e8e60c62c02be07541a7333ef49611792dd3 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 11 Jul 2022 11:26:49 -0700 Subject: [PATCH 56/84] testing github actions --- .../OptimizelyUserContextTests_Objc.m | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m index df4d840f..b8eb218b 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m @@ -38,17 +38,13 @@ - (void)setUp { // MARK: - UserContext -- (void)testUserContext { - OptimizelyUserContext *user = [[OptimizelyUserContext alloc] initWithOptimizely:self.optimizely - userId:kUserId - attributes:nil]; +- (void)testCXX_UserContext { + 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"]); @@ -64,7 +60,7 @@ - (void)testCreateUserContext { XCTAssert([user.attributes[@"age"] isEqualToString:@"18"]); } -- (void)testUserContext_setAttribute { +- (void)testCXX_UserContext_setAttribute { OptimizelyUserContext *user = [self.optimizely createUserContextWithUserId:kUserId attributes:nil]; [user setAttributeWithKey:@"country" value:@"US"]; [user setAttributeWithKey:@"age" value:@"18"]; From e0eadb2dcd24173bd4a8caa188d639af5d7c4806 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 11 Jul 2022 15:21:19 -0700 Subject: [PATCH 57/84] remove odp event auto retries --- Sources/ODP/OdpEventManager.swift | 12 ++++++------ .../OptimizelyUserContextTests_Objc.m | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/ODP/OdpEventManager.swift b/Sources/ODP/OdpEventManager.swift index 7a7e19de..e81327f6 100644 --- a/Sources/ODP/OdpEventManager.swift +++ b/Sources/ODP/OdpEventManager.swift @@ -22,7 +22,7 @@ class OdpEventManager { let zaiusMgr: ZaiusRestApiManager let maxQueueSize = 100 - let maxFailureCount = 3 + let maxFailureCount = 1 let queueLock: DispatchQueue let eventQueue: DataStoreQueueStackImpl @@ -110,10 +110,6 @@ class OdpEventManager { flush() } - func clearEvents() { - - } - func flush() { guard odpConfig.odpServiceIntegrated else { // clean up all pending events if datafile has no ODP public key (not integrated) @@ -145,10 +141,14 @@ class OdpEventManager { while let events: [OdpEvent] = self.eventQueue.getFirstItems(count: maxBatchEvents) { let numEvents = events.count + // multiple retires are disabled for now (maxFailureCount = 1) + // - this may be too much since they'll be retried any way when next events arrive. + // - also, no guaranee on success after multiple retris, so it helps minimal with extra complexity. + // we've exhuasted our failure count. Give up and try the next time a event // is queued or someone calls flush (changed to >= so that retried exactly "maxFailureCount" times). if failureCount >= self.maxFailureCount { - self.logger.e("ODP: Failed to send event with max retried") + self.logger.i("ODP: Failed to send event with max retried") break } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m index b8eb218b..26fb7f36 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Objc.m @@ -38,7 +38,7 @@ - (void)setUp { // MARK: - UserContext -- (void)testCXX_UserContext { +- (void)testUserContext { OptimizelyUserContext *user = [self.optimizely createUserContextWithUserId:kUserId attributes:nil]; XCTAssert([user.optimizely isEqual:self.optimizely]); XCTAssert([user.userId isEqualToString:kUserId]); @@ -60,7 +60,7 @@ - (void)testCreateUserContext { XCTAssert([user.attributes[@"age"] isEqualToString:@"18"]); } -- (void)testCXX_UserContext_setAttribute { +- (void)testUserContext_setAttribute { OptimizelyUserContext *user = [self.optimizely createUserContextWithUserId:kUserId attributes:nil]; [user setAttributeWithKey:@"country" value:@"US"]; [user setAttributeWithKey:@"age" value:@"18"]; From 72deb05c8e395024cdf695622aee61d0d16418d4 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 11 Jul 2022 16:16:02 -0700 Subject: [PATCH 58/84] fix disableOdp configuration --- Sources/ODP/OdpManager.swift | 6 +++--- Sources/ODP/OptimizelySdkSettings.swift | 8 ++++---- Sources/Optimizely/OptimizelyClient.swift | 2 +- .../OptimizelyClientTests_ODP.swift | 12 ++++++------ .../OdpEventManagerTests.swift | 3 ++- Tests/OptimizelyTests-Common/OdpManagerTests.swift | 10 +++++----- .../OptimizelyUserContextTests_ODP.swift | 2 +- 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Sources/ODP/OdpManager.swift b/Sources/ODP/OdpManager.swift index 633b7273..2b537524 100644 --- a/Sources/ODP/OdpManager.swift +++ b/Sources/ODP/OdpManager.swift @@ -31,17 +31,17 @@ class OdpManager { } init(sdkKey: String, - enable: Bool, + disable: Bool, cacheSize: Int, cacheTimeoutInSecs: Int, segmentManager: OdpSegmentManager? = nil, eventManager: OdpEventManager? = nil) { - self.enabled = enable + self.enabled = !disable self.odpConfig = OdpConfig() self.vuidManager = OdpVuidManager.shared - if enable { + if enabled { self.segmentManager = segmentManager ?? OdpSegmentManager(cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeoutInSecs, odpConfig: odpConfig) diff --git a/Sources/ODP/OptimizelySdkSettings.swift b/Sources/ODP/OptimizelySdkSettings.swift index cd0e06d8..6e68bf4e 100644 --- a/Sources/ODP/OptimizelySdkSettings.swift +++ b/Sources/ODP/OptimizelySdkSettings.swift @@ -21,14 +21,14 @@ public struct OptimizelySdkSettings { let segmentsCacheSize: Int /// timeout in seconds (default = 600) of audience segments cache (optional) let segmentsCacheTimeoutInSecs: Int - /// set this flag to false (default = true) to disable ODP features - let enableOdp: Bool + /// set this flag to true (default = false) to disable ODP features + let disableOdp: Bool public init(segmentsCacheSize: Int = 100, segmentsCacheTimeoutInSecs: Int = 600, - enableOdp: Bool = true) { + disableOdp: Bool = false) { self.segmentsCacheSize = segmentsCacheSize self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs - self.enableOdp = enableOdp + self.disableOdp = disableOdp } } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 6acf05d6..424cdc0b 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -83,7 +83,7 @@ open class OptimizelyClient: NSObject { self.defaultDecideOptions = defaultDecideOptions ?? [] self.odpManager = OdpManager(sdkKey: sdkKey, - enable: sdkSettings.enableOdp, + disable: sdkSettings.disableOdp, cacheSize: sdkSettings.segmentsCacheSize, cacheTimeoutInSecs: sdkSettings.segmentsCacheTimeoutInSecs) diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift index eaa46694..d4e24b83 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift @@ -44,13 +44,13 @@ class OptimizelyClientTests_ODP: XCTestCase { XCTAssertEqual(12, optimizely.odpManager.segmentManager?.segmentsCache.size) XCTAssertEqual(345, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) - sdkSettings = OptimizelySdkSettings(enableOdp: false) + sdkSettings = OptimizelySdkSettings(disableOdp: true) optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) XCTAssertEqual(false, optimizely.odpManager.enabled) } func testSendOdpEvent() { - let odpManager = MockOdpManager(sdkKey: "any", enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + let odpManager = MockOdpManager(sdkKey: "any", disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) optimizely.odpManager = odpManager optimizely.sendOdpEvent(type: "t1", action: "a1", identifiers: ["k1": "v1"], data: ["k2": "v2"]) @@ -77,7 +77,7 @@ class OptimizelyClientTests_ODP: XCTestCase { // MARK: - OdpConfig Update func testUpdateOpdConfigCalled_synchronous_success() { - let odpManager = MockOdpManager(sdkKey: "any", enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + let odpManager = MockOdpManager(sdkKey: "any", disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) optimizely.odpManager = odpManager XCTAssertNil(odpManager.apiKey) @@ -91,7 +91,7 @@ class OptimizelyClientTests_ODP: XCTestCase { } func testUpdateOpdConfigCalled_synchronous_failure() { - let odpManager = MockOdpManager(sdkKey: "any", enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + let odpManager = MockOdpManager(sdkKey: "any", disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) optimizely.odpManager = odpManager XCTAssertNil(odpManager.apiKey) @@ -107,7 +107,7 @@ class OptimizelyClientTests_ODP: XCTestCase { func testUpdateOpdConfigCalled_asynchronous_success() { let sdkKey = "valid" let optimizely = OptimizelyClient(sdkKey: sdkKey, datafileHandler: MockDatafileHandler()) - let odpManager = MockOdpManager(sdkKey: sdkKey, enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + let odpManager = MockOdpManager(sdkKey: sdkKey, disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) optimizely.odpManager = odpManager XCTAssertNil(odpManager.apiKey) @@ -126,7 +126,7 @@ class OptimizelyClientTests_ODP: XCTestCase { func testUpdateOpdConfigCalled_asynchronous_failure() { let sdkKey = "invalid" let optimizely = OptimizelyClient(sdkKey: sdkKey, datafileHandler: MockDatafileHandler()) - let odpManager = MockOdpManager(sdkKey: sdkKey, enable: true, cacheSize: 12, cacheTimeoutInSecs: 123) + let odpManager = MockOdpManager(sdkKey: sdkKey, disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) optimizely.odpManager = odpManager XCTAssertNil(odpManager.apiKey) diff --git a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift index c75762fc..fcc43ffa 100644 --- a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift @@ -249,7 +249,8 @@ class OdpEventManagerTests: XCTestCase { manager.flush() sleep(1) - XCTAssertEqual(3, apiManager.receivedBatchEvents.count, "should be retried 3 times (a batch of 2 events)") + let maxRetries = 1 // multiple retries disabled + XCTAssertEqual(maxRetries, apiManager.receivedBatchEvents.count, "should be retried \(maxRetries) times (a batch of 2 events)") XCTAssertEqual(2, manager.eventQueue.count, "the events should remain after giving up") } diff --git a/Tests/OptimizelyTests-Common/OdpManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift index a1d7b5f2..9bc34477 100644 --- a/Tests/OptimizelyTests-Common/OdpManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -32,7 +32,7 @@ class OdpManagerTests: XCTestCase { odpConfig: odpConfig) eventManager = MockOdpEventManager(sdkKey: sdkKey, odpConfig: odpConfig) manager = OdpManager(sdkKey: sdkKey, - enable: true, + disable: false, cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeout, segmentManager: segmentManager, @@ -47,16 +47,16 @@ class OdpManagerTests: XCTestCase { func testConfigurations_cache() { let manager = OdpManager(sdkKey: sdkKey, - enable: true, + disable: false, cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeout) XCTAssertEqual(manager.segmentManager?.segmentsCache.size, cacheSize) XCTAssertEqual(manager.segmentManager?.segmentsCache.timeoutInSecs, cacheTimeout) } - func testConfigurations_enable() { + func testConfigurations_disableOdp() { let manager = OdpManager(sdkKey: sdkKey, - enable: false, + disable: true, cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeout) @@ -142,7 +142,7 @@ class OdpManagerTests: XCTestCase { func testUpdateOdpConfig_odpConfigPropagatedProperly() { let manager = OdpManager(sdkKey: sdkKey, - enable: true, + disable: false, cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeout) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index a0881b60..005e2675 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -29,7 +29,7 @@ class OptimizelyUserContextTests_ODP: XCTestCase { let sdkKey = OTUtils.randomSdkKey override func setUp() { - odpManager = MockOdpManager(sdkKey: sdkKey, enable: true, cacheSize: 10, cacheTimeoutInSecs: 10) + odpManager = MockOdpManager(sdkKey: sdkKey, disable: false, cacheSize: 10, cacheTimeoutInSecs: 10) optimizely = OptimizelyClient(sdkKey: sdkKey) optimizely.odpManager = odpManager From cee47dc0a62b26f8f4e4c8180b951fd30c0f4aab Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 12 Jul 2022 15:42:31 -0700 Subject: [PATCH 59/84] fix event data device values --- .../xcschemes/DemoSwiftwatchOS.xcscheme | 25 +++---------- Sources/ODP/OdpEventManager.swift | 4 +- Sources/Utils/Utils.swift | 32 ++++++++++++---- .../OdpEventManagerTests.swift | 37 +++++++++++++++++-- 4 files changed, 66 insertions(+), 32 deletions(-) diff --git a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme index 7aa2b41d..1ebde8af 100644 --- a/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme +++ b/DemoSwiftApp/DemoSwiftApp.xcodeproj/xcshareddata/xcschemes/DemoSwiftwatchOS.xcscheme @@ -54,10 +54,8 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - - - - - + diff --git a/Sources/ODP/OdpEventManager.swift b/Sources/ODP/OdpEventManager.swift index e81327f6..01121fa0 100644 --- a/Sources/ODP/OdpEventManager.swift +++ b/Sources/ODP/OdpEventManager.swift @@ -78,9 +78,9 @@ class OdpEventManager { "data_source_version": Utils.sdkVersion, // "3.10.2" // [optional] client sdks only - "os": "iOS", // ("iOS", "Android", "Mac OS", "Windows", "Linux", ...) + "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", “PC”, "Other") + "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] diff --git a/Sources/Utils/Utils.swift b/Sources/Utils/Utils.swift index d26bcace..c027ac58 100644 --- a/Sources/Utils/Utils.swift +++ b/Sources/Utils/Utils.swift @@ -23,16 +23,34 @@ class Utils { 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 let osVersion = UIDevice.current.systemVersion static let deviceModel = UIDevice.current.model static var deviceType: String { - switch UIDevice.current.userInterfaceIdiom { - case .phone: return "Phone" - case .pad: return "Tablet" - case .tv: return "Smart TV" - case .mac: return "PC" - default: return "Other" - } + // 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() diff --git a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift index fcc43ffa..bc69f95f 100644 --- a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift @@ -55,10 +55,11 @@ class OdpEventManagerTests: XCTestCase { data: customData) XCTAssertEqual(1, manager.eventQueue.count) - sleep(3) + 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) @@ -69,7 +70,10 @@ class OdpEventManagerTests: XCTestCase { 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) @@ -103,7 +107,7 @@ class OdpEventManagerTests: XCTestCase { sleep(1) XCTAssertEqual(0, manager.eventQueue.count, "flushed since apiKey is ready") } - + // MARK: - flush func testFlush_apiKey() { @@ -296,11 +300,36 @@ class OdpEventManagerTests: XCTestCase { 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"] as! String) == "iOS") XCTAssert((data["os_version"] as! String).count > 3) - XCTAssert((data["device_type"] 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"]) From fdb2e14eb7df0ad73ed233273b0c141f657c6801 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 12 Jul 2022 16:30:49 -0700 Subject: [PATCH 60/84] fix device info for watchOS --- Sources/Utils/Utils.swift | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Sources/Utils/Utils.swift b/Sources/Utils/Utils.swift index c027ac58..9179d21e 100644 --- a/Sources/Utils/Utils.swift +++ b/Sources/Utils/Utils.swift @@ -15,7 +15,11 @@ // import Foundation +#if os(watchOS) +import WatchKit +#else import UIKit +#endif class Utils { @@ -36,8 +40,23 @@ class Utils { return "Other" #endif } - static let osVersion = UIDevice.current.systemVersion - static let deviceModel = UIDevice.current.model + + 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) From 57a3c3fa78de79c41cd40635e9aab5092541f5d0 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 12 Jul 2022 17:22:38 -0700 Subject: [PATCH 61/84] clean up user context init --- .../OptimizelyUserContext.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 139556d0..9bcd4fae 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -67,25 +67,23 @@ 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 convenience init(optimizely: OptimizelyClient, + public init(optimizely: OptimizelyClient, userId: String, attributes: [String: Any?]? = nil) { - self.init(optimizely: optimizely, userId: userId, attributes: attributes ?? [:]) - self.optimizely?.identifyUserToOdp(userId: userId) - } - - init(optimizely: OptimizelyClient, - userId: String, - attributes: [String: Any?]) { self.optimizely = optimizely self.userId = userId let lock = DispatchQueue(label: "user-context") - self.atomicAttributes = AtomicProperty(property: attributes, lock: lock) + self.atomicAttributes = AtomicProperty(property: attributes ?? [:], lock: lock) self.atomicForcedDecisions = AtomicProperty(property: nil, lock: lock) self.atomicQualifiedSegments = AtomicProperty(property: nil, lock: lock) + + // 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 From cafceae303a7d4faadb78a5d01d9c75f854a8d1b Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 12 Jul 2022 18:00:23 -0700 Subject: [PATCH 62/84] refact OptimizelyUserContext --- .../OptimizelyUserContext.swift | 21 +++++++++++++------ .../OptimizelyUserContextTests_ODP.swift | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 9bcd4fae..aa85c821 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -46,7 +46,7 @@ public class OptimizelyUserContext { 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.atomicForcedDecisions.property = fds @@ -67,9 +67,16 @@ 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, + 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 @@ -77,10 +84,12 @@ public class OptimizelyUserContext { self.atomicAttributes = AtomicProperty(property: attributes ?? [:], lock: lock) self.atomicForcedDecisions = AtomicProperty(property: nil, lock: lock) self.atomicQualifiedSegments = AtomicProperty(property: nil, lock: lock) - - // async call so event building overhead is not blocking context creation - lock.async { - self.optimizely?.identifyUserToOdp(userId: userId) + + if identify { + // async call so event building overhead is not blocking context creation + lock.async { + self.optimizely?.identifyUserToOdp(userId: userId) + } } } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index 005e2675..5456dc28 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -40,6 +40,7 @@ class OptimizelyUserContextTests_ODP: XCTestCase { // MARK: - identify func testIdentifyCalledAutomatically() { + sleep(1) XCTAssertEqual(true, odpManager.identifyCalled, "identifyUser is implicitly called on UserContext init") XCTAssertEqual(kUserId, odpManager.userId) } From d19e10e5b6155b98448ee2c277b230743f024494 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 13 Jul 2022 09:21:54 -0700 Subject: [PATCH 63/84] fix lint for FSC old swift versions --- DemoSwiftApp/DemoSwiftApp.xcodeproj/project.pbxproj | 2 +- Sources/ODP/OdpVuidManager.swift | 2 +- Sources/Optimizely+Decide/OptimizelyUserContext.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/Sources/ODP/OdpVuidManager.swift b/Sources/ODP/OdpVuidManager.swift index 46787af5..27896dff 100644 --- a/Sources/ODP/OdpVuidManager.swift +++ b/Sources/ODP/OdpVuidManager.swift @@ -49,7 +49,7 @@ extension OdpVuidManager { return "optimizely-odp" } private var keyForVuid: String { - "vuid" + return "vuid" } private func load() -> String { diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index aa85c821..8853ee62 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -81,7 +81,7 @@ public class OptimizelyUserContext { self.userId = userId let lock = DispatchQueue(label: "user-context") - self.atomicAttributes = AtomicProperty(property: attributes ?? [:], lock: lock) + self.atomicAttributes = AtomicProperty(property: attributes, lock: lock) self.atomicForcedDecisions = AtomicProperty(property: nil, lock: lock) self.atomicQualifiedSegments = AtomicProperty(property: nil, lock: lock) From 904a0e660776e12f7754769e086f70382338bdf2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 13 Jul 2022 09:48:28 -0700 Subject: [PATCH 64/84] add zero timeout support to LruCache --- Sources/ODP/LruCache.swift | 7 ++++-- Sources/ODP/OptimizelySdkSettings.swift | 4 ++-- .../LruCacheTests.swift | 24 +++++++++++++++---- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Sources/ODP/LruCache.swift b/Sources/ODP/LruCache.swift index 56dd9b37..6c3c5eb1 100644 --- a/Sources/ODP/LruCache.swift +++ b/Sources/ODP/LruCache.swift @@ -64,7 +64,7 @@ class LruCache { element = nil // check if all are stale and can be reset. - needReset = !isAllStale() + needReset = isAllStale() } } } @@ -138,12 +138,15 @@ class LruCache { } private func isValid(_ item: CacheElement) -> Bool { + if timeoutInSecs <= 0 { return true } return (Date.timeIntervalSinceReferenceDate - item.time) < Double(timeoutInSecs) } + /// Check if all items in the cache is too old + /// - Returns: true if the most recent item is stale private func isAllStale() -> Bool { guard let mostRecent = tail.prev else { return false } - return (Date.timeIntervalSinceReferenceDate - mostRecent.time) < Double(timeoutInSecs) + return !isValid(mostRecent) } } diff --git a/Sources/ODP/OptimizelySdkSettings.swift b/Sources/ODP/OptimizelySdkSettings.swift index 6e68bf4e..cb55abf8 100644 --- a/Sources/ODP/OptimizelySdkSettings.swift +++ b/Sources/ODP/OptimizelySdkSettings.swift @@ -17,9 +17,9 @@ import Foundation public struct OptimizelySdkSettings { - /// maximum size (default = 100) of audience segments cache (optional) + /// maximum size (default = 100) of audience segments cache (optional). Set to zero to disable caching. let segmentsCacheSize: Int - /// timeout in seconds (default = 600) of audience segments cache (optional) + /// timeout in seconds (default = 600) of audience segments cache (optional). Set to zero to disable timeout. let segmentsCacheTimeoutInSecs: Int /// set this flag to true (default = false) to disable ODP features let disableOdp: Bool diff --git a/Tests/OptimizelyTests-Common/LruCacheTests.swift b/Tests/OptimizelyTests-Common/LruCacheTests.swift index a009ab30..38db476e 100644 --- a/Tests/OptimizelyTests-Common/LruCacheTests.swift +++ b/Tests/OptimizelyTests-Common/LruCacheTests.swift @@ -69,7 +69,7 @@ class LruCacheTests: XCTestCase { XCTAssertEqual(cache.map.count, cache.size) } - func testZeroSize() { + func testSize_zero() { let cache = LruCache(size: 0, timeoutInSecs: 1000) XCTAssertNil(cache.lookup(key: 1)) @@ -91,7 +91,7 @@ extension LruCacheTests { 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) + sleep(2) // wait to expire cache.save(key: 4, value: 400) // [1, 2, 3] cache.save(key: 1, value: 101) // [1] @@ -101,6 +101,19 @@ extension LruCacheTests { 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)) + } + + func testAllStale() { let maxTimeout = 1 let cache = LruCache(size: 1000, timeoutInSecs: maxTimeout) @@ -108,9 +121,10 @@ extension LruCacheTests { 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) - - XCTAssertNil(cache.lookup(key: 1)) + sleep(2) // wait to expire + XCTAssertEqual(cache.map.count, 3) + + XCTAssertNil(cache.lookup(key: 1)) // this will reset cache (allStale) XCTAssert(cache.map.isEmpty, "cache should be reset when detected that all items are stale") } From 22008472a7585ea26974f5b700cc5cd08be4a70d Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 15 Jul 2022 12:09:51 -0700 Subject: [PATCH 65/84] clean up odp event retries --- DemoSwiftApp/AppDelegate.swift | 4 +- Sources/ODP/OdpEventManager.swift | 58 ++++++++----------- .../OdpEventManagerTests.swift | 5 +- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 75af7eae..479d3ccd 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -21,9 +21,9 @@ import Optimizely class AppDelegate: UIResponder, UIApplicationDelegate { let logLevel = OptimizelyLogLevel.debug - let sdkKey = "FCnSegiEkRry9rhVMroit4" + let sdkKey = "5L6MJs1JnPSfNgsGhw5Afu" let datafileName = "demoTestDatafile" - let featureKey = "decide_demo" + let featureKey = "buy_now_feature" let experimentKey = "background_experiment_decide" let eventKey = "sample_conversion" diff --git a/Sources/ODP/OdpEventManager.swift b/Sources/ODP/OdpEventManager.swift index 01121fa0..3349dae1 100644 --- a/Sources/ODP/OdpEventManager.swift +++ b/Sources/ODP/OdpEventManager.swift @@ -22,7 +22,6 @@ class OdpEventManager { let zaiusMgr: ZaiusRestApiManager let maxQueueSize = 100 - let maxFailureCount = 1 let queueLock: DispatchQueue let eventQueue: DataStoreQueueStackImpl @@ -131,57 +130,46 @@ class OdpEventManager { } } - // notify group used to ensure that the sendEvent is synchronous. + // sync group used to ensure that the sendEvent is synchronous. // used in flushEvents - let notify = DispatchGroup() + let sync = DispatchGroup() let maxBatchEvents = 10 - var failureCount = 0 while let events: [OdpEvent] = self.eventQueue.getFirstItems(count: maxBatchEvents) { let numEvents = events.count - // multiple retires are disabled for now (maxFailureCount = 1) + // multiple auto-retires are disabled for now // - this may be too much since they'll be retried any way when next events arrive. // - also, no guaranee on success after multiple retris, so it helps minimal with extra complexity. + + var odpError: OptimizelyError? - // we've exhuasted our failure count. Give up and try the next time a event - // is queued or someone calls flush (changed to >= so that retried exactly "maxFailureCount" times). - if failureCount >= self.maxFailureCount { - self.logger.i("ODP: Failed to send event with max retried") - break - } - - // make the send event synchronous. enter our notify - notify.enter() - + sync.enter() // make the send event synchronous. enter our notify self.zaiusMgr.sendOdpEvents(apiKey: odpApiKey, apiHost: odpApiHost, events: events) { error in - defer { - notify.leave() // our send is done. - } + odpError = error + sync.leave() // our send is done. + } + sync.wait() // wait for send completed + + if let error = odpError { + self.logger.e(error.reason) - if let error = error { - self.logger.e(error.reason) - - // retry only if needed (non-permanent) - if case .odpEventFailed(_, let canRetry) = error { - if canRetry { - failureCount += 1 - return - } else { - // permanent errors (400 response or invalid events, etc) - // discard these events so not they do not block following valid events - } + // 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) - failureCount = 0 } - - notify.wait() // wait for send completed + + removeStoredEvents(num: numEvents) } } } diff --git a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift index bc69f95f..073636ab 100644 --- a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift @@ -253,9 +253,8 @@ class OdpEventManagerTests: XCTestCase { manager.flush() sleep(1) - let maxRetries = 1 // multiple retries disabled - XCTAssertEqual(maxRetries, apiManager.receivedBatchEvents.count, "should be retried \(maxRetries) times (a batch of 2 events)") - XCTAssertEqual(2, manager.eventQueue.count, "the events should remain after giving up") + XCTAssertEqual(1, apiManager.receivedBatchEvents.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() { From 33f937345d77dd14da8f5174c65188d59d5781b1 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 15 Jul 2022 15:36:27 -0700 Subject: [PATCH 66/84] move integrations tests --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 2 +- .../IntegrationTests.swift | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Tests/{OptimizelyTests-APIs => OptimizelyTests-DataModel}/IntegrationTests.swift (100%) diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 1b628195..facea93d 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -2949,6 +2949,7 @@ 6E7519A022C5211100B2B157 /* VariableTests.swift */, 6E7519A122C5211100B2B157 /* FeatureFlagTests.swift */, 6E7519A222C5211100B2B157 /* AudienceTests.swift */, + 84640880281320F000CCF97D /* IntegrationTests.swift */, 6E7519A322C5211100B2B157 /* ConditionLeafTests.swift */, 6E7519A422C5211100B2B157 /* AudienceTests_Evaluate.swift */, 6E7519A522C5211100B2B157 /* UserAttributeTests_Evaluate.swift */, @@ -2992,7 +2993,6 @@ 6E7519B922C5211100B2B157 /* OptimizelyTests-APIs */ = { isa = PBXGroup; children = ( - 84640880281320F000CCF97D /* IntegrationTests.swift */, 6E7519BC22C5211100B2B157 /* OptimizelyErrorTests.swift */, 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */, 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */, diff --git a/Tests/OptimizelyTests-APIs/IntegrationTests.swift b/Tests/OptimizelyTests-DataModel/IntegrationTests.swift similarity index 100% rename from Tests/OptimizelyTests-APIs/IntegrationTests.swift rename to Tests/OptimizelyTests-DataModel/IntegrationTests.swift From 80f40da6773c8f9d604f4c376c86bd3f8d7e301c Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 20 Jul 2022 10:40:47 -0700 Subject: [PATCH 67/84] qualifedSegments set to nil when fetch fails --- Sources/Optimizely+Decide/OptimizelyUserContext.swift | 3 +++ .../OptimizelyUserContextTests_ODP.swift | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyUserContext.swift b/Sources/Optimizely+Decide/OptimizelyUserContext.swift index 8853ee62..0a3d1f13 100644 --- a/Sources/Optimizely+Decide/OptimizelyUserContext.swift +++ b/Sources/Optimizely+Decide/OptimizelyUserContext.swift @@ -193,6 +193,9 @@ extension OptimizelyUserContext { /// - 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 diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index 5456dc28..3913c129 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -77,6 +77,7 @@ class OptimizelyUserContextTests_ODP: XCTestCase { func testFetchQualifiedSegments_sdkNotReady() { user.optimizely = nil + user.qualifiedSegments = ["dummy"] let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments { segments, error in @@ -89,10 +90,11 @@ class OptimizelyUserContextTests_ODP: XCTestCase { } func testFetchQualifiedSegments_fetchFailed() { - let sem = DispatchSemaphore(value: 0) - + user.qualifiedSegments = ["dummy"] + // ODP apiKey is not available + let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments { segments, error in XCTAssertNotNil(error) XCTAssertNil(segments) From 2b15b5c66232363cab3970e8d729426485aa9857 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 25 Jul 2022 14:51:04 -0700 Subject: [PATCH 68/84] integrations foward compat test --- Tests/OptimizelyTests-DataModel/IntegrationTests.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests/OptimizelyTests-DataModel/IntegrationTests.swift b/Tests/OptimizelyTests-DataModel/IntegrationTests.swift index 5ceacdd0..39cdbd58 100644 --- a/Tests/OptimizelyTests-DataModel/IntegrationTests.swift +++ b/Tests/OptimizelyTests-DataModel/IntegrationTests.swift @@ -58,6 +58,15 @@ extension IntegrationTests { 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) From cd18ac0b22572e6279ee165b443af0315895eafa Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 25 Jul 2022 14:54:48 -0700 Subject: [PATCH 69/84] fix graphql request examples --- Sources/ODP/ZaiusGraphQLApiManager.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Sources/ODP/ZaiusGraphQLApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift index fc29dd67..4ddd9b9d 100644 --- a/Sources/ODP/ZaiusGraphQLApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -24,20 +24,19 @@ import Foundation [GraphQL Request] - // fetch all segments - curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql + // 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 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 + // 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 { + audiences(subset:["has_email", "has_email_opted_in", "push_on_sale"]) { edges { node { name state - description } } } @@ -55,14 +54,12 @@ import Foundation "node": { "name": "has_email", "state": "qualified", - "description": "Customers who have an email address (regardless of consent/reachability status)" } }, { "node": { "name": "has_email_opted_in", "state": "qualified", - "description": "Customers who have an email address, and it is opted-in" } }, ... From cefb5e26c724e532145e529af76a7b5ca796cd9e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 26 Jul 2022 13:21:54 -0700 Subject: [PATCH 70/84] add doc comments --- Sources/ODP/OptimizelySdkSettings.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/ODP/OptimizelySdkSettings.swift b/Sources/ODP/OptimizelySdkSettings.swift index cb55abf8..38afe4c3 100644 --- a/Sources/ODP/OptimizelySdkSettings.swift +++ b/Sources/ODP/OptimizelySdkSettings.swift @@ -17,13 +17,19 @@ import Foundation public struct OptimizelySdkSettings { - /// maximum size (default = 100) of audience segments cache (optional). Set to zero to disable caching. + /// The maximum size of audience segments cache - cache is disabled if this is set to zero. let segmentsCacheSize: Int - /// timeout in seconds (default = 600) of audience segments cache (optional). Set to zero to disable timeout. + /// The timeout in seconds of audience segments cache - timeout is disabled if this is set to zero. let segmentsCacheTimeoutInSecs: Int - /// set this flag to true (default = false) to disable ODP features + /// 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) { From 84ff4e15af2262f5c4a9ba6117f76750c246483c Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 26 Jul 2022 13:41:00 -0700 Subject: [PATCH 71/84] add guard for negative segments cache size --- Sources/ODP/LruCache.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ODP/LruCache.swift b/Sources/ODP/LruCache.swift index 6c3c5eb1..8efac891 100644 --- a/Sources/ODP/LruCache.swift +++ b/Sources/ODP/LruCache.swift @@ -46,7 +46,7 @@ class LruCache { } func lookup(key: K) -> V? { - if size == 0 { return nil } + if size <= 0 { return nil } var element: CacheElement? = nil var needReset = false @@ -78,7 +78,7 @@ class LruCache { } func save(key: K, value: V) { - if size == 0 { return } + if size <= 0 { return } queue.async(flags: .barrier) { let oldSegments = self.map[key] @@ -100,7 +100,7 @@ class LruCache { // read cache contents without order update func peek(key: K) -> V? { - if size == 0 { return nil } + if size <= 0 { return nil } var element: CacheElement? = nil queue.sync { @@ -110,7 +110,7 @@ class LruCache { } func reset() { - if size == 0 { return } + if size <= 0 { return } queue.sync { map = [K: CacheElement]() From 11b7dfe8d465cdc4c489de5e5a78118ad7ae3d65 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 27 Jul 2022 15:08:33 -0700 Subject: [PATCH 72/84] remve reset on allStale from segments cache --- Sources/ODP/LruCache.swift | 15 --------------- Tests/OptimizelyTests-Common/LruCacheTests.swift | 15 --------------- 2 files changed, 30 deletions(-) diff --git a/Sources/ODP/LruCache.swift b/Sources/ODP/LruCache.swift index 8efac891..329986e4 100644 --- a/Sources/ODP/LruCache.swift +++ b/Sources/ODP/LruCache.swift @@ -62,18 +62,10 @@ class LruCache { } else { map[key] = nil element = nil - - // check if all are stale and can be reset. - needReset = isAllStale() } } } - // reset is called outside the sync block to avoid deadlock - if needReset { - reset() - } - return element?.value } @@ -142,11 +134,4 @@ class LruCache { return (Date.timeIntervalSinceReferenceDate - item.time) < Double(timeoutInSecs) } - /// Check if all items in the cache is too old - /// - Returns: true if the most recent item is stale - private func isAllStale() -> Bool { - guard let mostRecent = tail.prev else { return false } - return !isValid(mostRecent) - } - } diff --git a/Tests/OptimizelyTests-Common/LruCacheTests.swift b/Tests/OptimizelyTests-Common/LruCacheTests.swift index 38db476e..4bc0ba23 100644 --- a/Tests/OptimizelyTests-Common/LruCacheTests.swift +++ b/Tests/OptimizelyTests-Common/LruCacheTests.swift @@ -113,21 +113,6 @@ extension LruCacheTests { XCTAssertEqual(200, cache.lookup(key: 2)) } - - func testAllStale() { - let maxTimeout = 1 - let cache = LruCache(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 - XCTAssertEqual(cache.map.count, 3) - - XCTAssertNil(cache.lookup(key: 1)) // this will reset cache (allStale) - XCTAssert(cache.map.isEmpty, "cache should be reset when detected that all items are stale") - } - } #endif From edf35958b789f30a083c4a34fc959c780360aec6 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 28 Jul 2022 08:46:36 -0700 Subject: [PATCH 73/84] clean up --- Sources/ODP/LruCache.swift | 3 +-- Sources/ODP/ZaiusGraphQLApiManager.swift | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/ODP/LruCache.swift b/Sources/ODP/LruCache.swift index 329986e4..f411245a 100644 --- a/Sources/ODP/LruCache.swift +++ b/Sources/ODP/LruCache.swift @@ -49,8 +49,7 @@ class LruCache { if size <= 0 { return nil } var element: CacheElement? = nil - var needReset = false - + queue.sync { element = map[key] diff --git a/Sources/ODP/ZaiusGraphQLApiManager.swift b/Sources/ODP/ZaiusGraphQLApiManager.swift index 4ddd9b9d..4f83951a 100644 --- a/Sources/ODP/ZaiusGraphQLApiManager.swift +++ b/Sources/ODP/ZaiusGraphQLApiManager.swift @@ -25,14 +25,14 @@ import Foundation [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 + 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 + 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"]) { + audiences(subset:["has_email","has_email_opted_in","push_on_sale"]) { edges { node { name From 7a218bdf0f529adc557717c9965f075eb49bbfc4 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 29 Jul 2022 16:14:44 -0700 Subject: [PATCH 74/84] fix vuid length to 32 --- Sources/ODP/OdpVuidManager.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ODP/OdpVuidManager.swift b/Sources/ODP/OdpVuidManager.swift index 27896dff..1c288cd5 100644 --- a/Sources/ODP/OdpVuidManager.swift +++ b/Sources/ODP/OdpVuidManager.swift @@ -28,7 +28,12 @@ class OdpVuidManager { } func makeVuid() -> String { - return "vuid_" + UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased() + 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 { From f60bb97bda28b24fdfc265563544d7962e60d064 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 3 Aug 2022 10:06:29 -0700 Subject: [PATCH 75/84] clean up demo app --- DemoSwiftApp/AppDelegate.swift | 10 +++++----- Sources/Data Model/ProjectConfig.swift | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/DemoSwiftApp/AppDelegate.swift b/DemoSwiftApp/AppDelegate.swift index 479d3ccd..a2304653 100644 --- a/DemoSwiftApp/AppDelegate.swift +++ b/DemoSwiftApp/AppDelegate.swift @@ -21,9 +21,9 @@ import Optimizely class AppDelegate: UIResponder, UIApplicationDelegate { let logLevel = OptimizelyLogLevel.debug - let sdkKey = "5L6MJs1JnPSfNgsGhw5Afu" + let sdkKey = "FCnSegiEkRry9rhVMroit4" let datafileName = "demoTestDatafile" - let featureKey = "buy_now_feature" + let featureKey = "decide_demo" let experimentKey = "background_experiment_decide" let eventKey = "sample_conversion" @@ -134,9 +134,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { 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) + //SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely) + //SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely) + //SamplesForAPI.checkAudienceSegments(optimizely: self.optimizely) } } diff --git a/Sources/Data Model/ProjectConfig.swift b/Sources/Data Model/ProjectConfig.swift index 00d7f7d9..2330f432 100644 --- a/Sources/Data Model/ProjectConfig.swift +++ b/Sources/Data Model/ProjectConfig.swift @@ -55,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 { From 4ab96362cef3727b2c13d09c5819f8dcf0bd740f Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 3 Aug 2022 16:24:18 -0700 Subject: [PATCH 76/84] reset segments cache on datafile update --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 74 +++++++++++++++---- Sources/ODP/OdpConfig.swift | 38 ++++++++-- Sources/ODP/OdpEventManager.swift | 2 +- Sources/ODP/OdpManager.swift | 16 +++- Sources/ODP/OdpSegmentManager.swift | 17 ++++- Sources/Optimizely/OptimizelyClient.swift | 20 ++--- .../OptimizelyClientTests_ODP.swift | 2 +- .../OdpEventManagerTests.swift | 22 +++--- .../OdpManagerTests.swift | 21 ++---- .../OdpSegmentManagerTests.swift | 15 ++-- .../OdpZaiusGraphQLApiManagerTests.swift | 2 +- .../OptimizelyUserContextTests_ODP.swift | 60 +++++++++------ .../decide_audience_segments.json | 0 .../odp/odp_integrated_no_segments.json | 22 ++++++ 14 files changed, 209 insertions(+), 102 deletions(-) rename Tests/TestData/{decide => odp}/decide_audience_segments.json (100%) create mode 100644 Tests/TestData/odp/odp_integrated_no_segments.json diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index facea93d..02791b8e 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1790,6 +1790,30 @@ 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 */; }; 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 */; }; @@ -1950,12 +1974,6 @@ 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 */; }; - 84F6BACC27FCFAD7004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; - 84F6BAD727FCFB14004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; - 84F6BAD827FCFB15004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; - 84F6BAD927FCFB17004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; - 84F6BADA27FCFB18004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; - 84F6BADB27FCFB21004BE62A /* decide_audience_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */; }; 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 */; }; @@ -2373,6 +2391,8 @@ 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 = ""; }; 848617C72863DC2700B7F41B /* OdpSegmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OdpSegmentManager.swift; sourceTree = ""; }; 848617D82863E21200B7F41B /* ZaiusGraphQLApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusGraphQLApiManager.swift; sourceTree = ""; }; 848617D92863E21200B7F41B /* ZaiusRestApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZaiusRestApiManager.swift; sourceTree = ""; }; @@ -2390,7 +2410,6 @@ 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 = ""; }; - 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_audience_segments.json; 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 = ""; }; @@ -2838,6 +2857,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 */, @@ -3042,7 +3062,6 @@ isa = PBXGroup; children = ( 6EF8DE0524B8DA58008B9488 /* decide_datafile.json */, - 84F6BAC627FCFAD7004BE62A /* decide_audience_segments.json */, ); path = decide; sourceTree = ""; @@ -3055,6 +3074,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 = ( @@ -3519,7 +3547,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 */, @@ -3548,6 +3578,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 */, @@ -3556,13 +3587,13 @@ 6EE59299264DF4990013AD66 /* bucketer_test2.json in Resources */, 6EE592AC264DF4990013AD66 /* unsupported_datafile.json in Resources */, 6EE592A8264DF4990013AD66 /* optimizely_6372300739_v4.json in Resources */, - 84F6BADB27FCFB21004BE62A /* decide_audience_segments.json in Resources */, 6EE5929F264DF4990013AD66 /* rollout_bucketing.json in Resources */, 6EE592A0264DF4990013AD66 /* feature_management_experiment_bucketing.json in Resources */, 6EE59298264DF4990013AD66 /* bucketer_test.json in Resources */, 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 */, ); @@ -3606,7 +3637,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 */, @@ -3625,6 +3658,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 */, @@ -3637,9 +3671,9 @@ 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 */, - 84F6BAD727FCFB14004BE62A /* decide_audience_segments.json in Resources */, 6E12B20822C55A270005E9E6 /* audience_targeting.json in Resources */, 6E12B1FB22C55A270005E9E6 /* feature_rollout_toggle_on.json in Resources */, 6E12B1FC22C55A270005E9E6 /* feature_rollout_toggle_off.json in Resources */, @@ -3688,7 +3722,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 */, @@ -3717,6 +3753,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 */, @@ -3725,13 +3762,13 @@ 6E12B27522C55A290005E9E6 /* feature_experiments.json in Resources */, 6E12B28722C55A290005E9E6 /* bot_filtering_enabled.json in Resources */, 6E12B28422C55A290005E9E6 /* bucketer_test.json in Resources */, - 84F6BAD927FCFB17004BE62A /* decide_audience_segments.json in Resources */, 6E12B28322C55A290005E9E6 /* empty_datafile.json in Resources */, 6E12B28522C55A290005E9E6 /* bucketer_test2.json in Resources */, 6E12B28222C55A290005E9E6 /* feature_exp.json in Resources */, 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 */, ); @@ -3758,6 +3795,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 */, @@ -3766,13 +3804,13 @@ 6E12B28D22C55A2A0005E9E6 /* feature_experiments.json in Resources */, 6E12B29F22C55A2A0005E9E6 /* bot_filtering_enabled.json in Resources */, 6E12B29C22C55A2A0005E9E6 /* bucketer_test.json in Resources */, - 84F6BADA27FCFB18004BE62A /* decide_audience_segments.json in Resources */, 6E12B29B22C55A2A0005E9E6 /* empty_datafile.json in Resources */, 6E12B29D22C55A2A0005E9E6 /* bucketer_test2.json in Resources */, 6E12B29A22C55A2A0005E9E6 /* feature_exp.json in Resources */, 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 */, ); @@ -3799,6 +3837,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 */, @@ -3807,13 +3846,13 @@ 6E12B1E522C55A260005E9E6 /* feature_experiments.json in Resources */, 6E12B1F722C55A260005E9E6 /* bot_filtering_enabled.json in Resources */, 6E12B1F422C55A260005E9E6 /* bucketer_test.json in Resources */, - 84F6BACC27FCFAD7004BE62A /* decide_audience_segments.json in Resources */, 6E12B1F322C55A260005E9E6 /* empty_datafile.json in Resources */, 6E12B1F522C55A260005E9E6 /* bucketer_test2.json in Resources */, 6E12B1F222C55A260005E9E6 /* feature_exp.json in Resources */, 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 */, ); @@ -3840,6 +3879,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 */, @@ -3848,13 +3888,13 @@ 6E12B21522C55A270005E9E6 /* feature_experiments.json in Resources */, 6E12B22722C55A270005E9E6 /* bot_filtering_enabled.json in Resources */, 6E12B22422C55A270005E9E6 /* bucketer_test.json in Resources */, - 84F6BAD827FCFB15004BE62A /* decide_audience_segments.json in Resources */, 6E12B22322C55A270005E9E6 /* empty_datafile.json in Resources */, 6E12B22522C55A270005E9E6 /* bucketer_test2.json in Resources */, 6E12B22222C55A270005E9E6 /* feature_exp.json in Resources */, 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 */, ); @@ -3891,7 +3931,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 */, @@ -3930,7 +3972,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 */, @@ -3976,7 +4020,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 */, diff --git a/Sources/ODP/OdpConfig.swift b/Sources/ODP/OdpConfig.swift index 094d8eff..61158a94 100644 --- a/Sources/ODP/OdpConfig.swift +++ b/Sources/ODP/OdpConfig.swift @@ -23,22 +23,30 @@ class OdpConfig { private var _apiKey: String? /// assumed integrated by default (set to false when datafile has no ODP key/host settings) private var _odpServiceIntegrated: Bool + /// an array of all ODP segments used in the current datafile (associated with apiHost/apiKey). + private var _segmentsToCheck: [String] let queue = DispatchQueue(label: "odpConfig") - init(apiKey: String? = nil, apiHost: String? = nil, odpServiceIntegrated: Bool = true) { + init(apiKey: String? = nil, apiHost: String? = nil, segmentsToCheck: [String] = [], odpServiceIntegrated: Bool = true) { self._apiKey = apiKey self._apiHost = apiHost - self._odpServiceIntegrated = odpServiceIntegrated + self._segmentsToCheck = segmentsToCheck + self._odpServiceIntegrated = odpServiceIntegrated // initially assumed true until the first datafile is parsed } - func update(apiKey: String?, apiHost: String?) { - self.apiKey = apiKey - self.apiHost = apiHost - + func update(apiKey: String?, apiHost: String?, segmentsToCheck: [String]) -> Bool { // disable future event queueing if datafile has no ODP integrations. - self.odpServiceIntegrated = (apiKey != nil) && (apiHost != nil) + + if self.apiKey == apiKey, self.apiHost == apiHost, self.segmentsToCheck == segmentsToCheck { + return false + } + + self.apiKey = apiKey + self.apiHost = apiHost + self.segmentsToCheck = segmentsToCheck + return true } } @@ -76,6 +84,21 @@ extension OdpConfig { } } + var segmentsToCheck: [String] { + get { + var value = [String]() + queue.sync { + value = _segmentsToCheck + } + return value + } + set { + queue.async { + self._segmentsToCheck = newValue + } + } + } + var odpServiceIntegrated: Bool { get { var value = false @@ -90,6 +113,5 @@ extension OdpConfig { } } } - } diff --git a/Sources/ODP/OdpEventManager.swift b/Sources/ODP/OdpEventManager.swift index 3349dae1..31ff7158 100644 --- a/Sources/ODP/OdpEventManager.swift +++ b/Sources/ODP/OdpEventManager.swift @@ -111,7 +111,7 @@ class OdpEventManager { func flush() { guard odpConfig.odpServiceIntegrated else { - // clean up all pending events if datafile has no ODP public key (not integrated) + // clean up all pending events if datafile is ready but has no ODP public key (not integrated) _ = eventQueue.removeFirstItems(count: self.maxQueueSize) return } diff --git a/Sources/ODP/OdpManager.swift b/Sources/ODP/OdpManager.swift index 2b537524..17b30bb6 100644 --- a/Sources/ODP/OdpManager.swift +++ b/Sources/ODP/OdpManager.swift @@ -52,7 +52,6 @@ class OdpManager { } func fetchQualifiedSegments(userId: String, - segmentsToCheck: [String], options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { guard enabled else { @@ -65,7 +64,6 @@ class OdpManager { segmentManager?.fetchQualifiedSegments(userKey: userKey, userValue: userValue, - segmentsToCheck: segmentsToCheck, options: options, completionHandler: completionHandler) } @@ -93,13 +91,23 @@ class OdpManager { eventManager?.sendEvent(type: type, action: action, identifiers: identifiersWithVuid, data: data) } - func updateOdpConfig(apiKey: String?, apiHost: String?) { + func updateOdpConfig(apiKey: String?, apiHost: String?, segmentsToCheck: [String]) { guard enabled else { return } - odpConfig.update(apiKey: apiKey, apiHost: apiHost) + // flush old events before updating odp integration values eventManager?.flush() + + let configChanged = odpConfig.update(apiKey: apiKey, apiHost: apiHost, segmentsToCheck: segmentsToCheck) + + if configChanged { + // reset events cache when odp integration or segmentsToCheck changed + segmentManager?.reset() + + // flush old events with the new integration values if events still remain in the queue (when we get the first datafile ready) + eventManager?.flush() + } } } diff --git a/Sources/ODP/OdpSegmentManager.swift b/Sources/ODP/OdpSegmentManager.swift index e2489930..db098a9d 100644 --- a/Sources/ODP/OdpSegmentManager.swift +++ b/Sources/ODP/OdpSegmentManager.swift @@ -18,9 +18,9 @@ import Foundation class OdpSegmentManager { let odpConfig: OdpConfig - let zaiusMgr: ZaiusGraphQLApiManager let segmentsCache: LruCache - + var zaiusMgr: ZaiusGraphQLApiManager + let logger = OPTLoggerFactory.getLogger() init(cacheSize: Int, @@ -36,7 +36,6 @@ class OdpSegmentManager { func fetchQualifiedSegments(userKey: String, userValue: String, - segmentsToCheck: [String], options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { guard let odpApiKey = odpConfig.apiKey, let odpApiHost = odpConfig.apiHost else { @@ -44,13 +43,20 @@ class OdpSegmentManager { return } + // emtpy 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 { - segmentsCache.reset() + reset() } if !ignoreCache { @@ -75,6 +81,9 @@ class OdpSegmentManager { } } + func reset() { + segmentsCache.reset() + } } // MARK: - Utils diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 424cdc0b..9d98f2e0 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -200,7 +200,11 @@ open class OptimizelyClient: NSObject { do { self.config = try ProjectConfig(datafile: datafile) - odpManager.updateOdpConfig(apiKey: config?.publicKeyForODP, apiHost: config?.hostForODP) + guard let config = self.config else { throw OptimizelyError.dataFileInvalid } + + odpManager.updateOdpConfig(apiKey: config.publicKeyForODP, + apiHost: config.hostForODP, + segmentsToCheck: config.allSegments) datafileHandler?.startUpdates(sdkKey: self.sdkKey) { data in // new datafile came in @@ -955,20 +959,8 @@ extension OptimizelyClient { func fetchQualifiedSegments(userId: String, options: [OptimizelySegmentOption], - completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { - guard let segmentsToCheck = config?.allSegments else { - completionHandler(nil, .sdkNotReady) - return - } - - // emtpy segmentsToCheck (no ODP audiences found in datafile) is not an error. - guard segmentsToCheck.count > 0 else { - completionHandler([], nil) - return - } - + completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { odpManager.fetchQualifiedSegments(userId: userId, - segmentsToCheck: segmentsToCheck, options: options, completionHandler: completionHandler) } diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift index d4e24b83..313bb5bc 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift @@ -165,7 +165,7 @@ extension OptimizelyClientTests_ODP { self.eventData = data } - override func updateOdpConfig(apiKey: String?, apiHost: String?) { + override func updateOdpConfig(apiKey: String?, apiHost: String?, segmentsToCheck: [String]) { self.apiKey = apiKey self.apiHost = apiHost } diff --git a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift index 073636ab..eb5c1374 100644 --- a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift @@ -93,7 +93,7 @@ class OdpEventManagerTests: XCTestCase { func testSendEvent_apiKey() { odpConfig = OdpConfig() - odpConfig.update(apiKey: "valid", apiHost: "host") + odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager = OdpEventManager(sdkKey: "any", odpConfig: odpConfig, @@ -125,7 +125,7 @@ class OdpEventManagerTests: XCTestCase { // apiKey is ready - odpConfig.update(apiKey: "valid", apiHost: "host") + odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) @@ -140,7 +140,7 @@ class OdpEventManagerTests: XCTestCase { ] manager.dispatch(events[0]) - odpConfig.update(apiKey: "valid", apiHost: "host") + odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) @@ -160,7 +160,7 @@ class OdpEventManagerTests: XCTestCase { manager.dispatch(e) } - odpConfig.update(apiKey: "valid", apiHost: "host") + odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) @@ -177,7 +177,7 @@ class OdpEventManagerTests: XCTestCase { manager.dispatch(e) } - odpConfig.update(apiKey: "valid", apiHost: "host") + odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) @@ -188,7 +188,7 @@ class OdpEventManagerTests: XCTestCase { } func testFlush_emptyQueue() { - odpConfig.update(apiKey: "valid", apiHost: "host") + odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) @@ -221,7 +221,7 @@ class OdpEventManagerTests: XCTestCase { XCTAssertEqual(0, apiManager1.receivedBatchEvents.count) XCTAssertEqual(0, apiManager2.receivedBatchEvents.count) - odpConfig1.update(apiKey: "valid", apiHost: "host") + odpConfig1.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager1.flush() sleep(1) @@ -229,7 +229,7 @@ class OdpEventManagerTests: XCTestCase { XCTAssertEqual(2, apiManager1.receivedBatchEvents[0].count) XCTAssertEqual(0, apiManager2.receivedBatchEvents.count) - odpConfig2.update(apiKey: "valid", apiHost: "host") + odpConfig2.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager2.flush() sleep(1) @@ -249,7 +249,7 @@ class OdpEventManagerTests: XCTestCase { manager.dispatch(e) } - odpConfig.update(apiKey: "valid-key-retry-error", apiHost: "host") + odpConfig.update(apiKey: "valid-key-retry-error", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) @@ -265,7 +265,7 @@ class OdpEventManagerTests: XCTestCase { manager.dispatch(e) } - odpConfig.update(apiKey: "invalid-key-no-retry", apiHost: "host") + odpConfig.update(apiKey: "invalid-key-no-retry", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) @@ -278,7 +278,7 @@ class OdpEventManagerTests: XCTestCase { // MARK: - OdpConfig func testOdpConfig() { - odpConfig.update(apiKey: "test-key", apiHost: "test-host") + odpConfig.update(apiKey: "test-key", apiHost: "test-host", segmentsToCheck: []) manager = OdpEventManager(sdkKey: "any", odpConfig: odpConfig, diff --git a/Tests/OptimizelyTests-Common/OdpManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift index 9bc34477..acbfb103 100644 --- a/Tests/OptimizelyTests-Common/OdpManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -63,14 +63,14 @@ class OdpManagerTests: XCTestCase { XCTAssertTrue(manager.vuid.starts(with: "vuid_"), "vuid should be serverved even when ODP is disabled.") let sem = DispatchSemaphore(value: 0) - manager.fetchQualifiedSegments(userId: "user1", segmentsToCheck: [], options: []) { segments, error in + 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") + manager.updateOdpConfig(apiKey: "valid", apiHost: "host", segmentsToCheck: []) XCTAssertNil(manager.odpConfig.apiKey) XCTAssertNil(manager.odpConfig.apiHost) @@ -87,19 +87,17 @@ class OdpManagerTests: XCTestCase { func testFetchQualifiedSegments() { let vuid = "vuid_123" - manager.fetchQualifiedSegments(userId: vuid, segmentsToCheck: ["seg-1"], options: [.ignoreCache]) { _, _ in } + manager.fetchQualifiedSegments(userId: vuid, options: [.ignoreCache]) { _, _ in } XCTAssertEqual(segmentManager.receivedUserKey, "vuid") XCTAssertEqual(segmentManager.receivedUserValue, vuid) - XCTAssertEqual(segmentManager.receivedSegmentsToCheck, ["seg-1"]) XCTAssertEqual(segmentManager.receivedOptions, [.ignoreCache]) let userId = "user-1" - manager.fetchQualifiedSegments(userId: userId, segmentsToCheck: [], options: []) { _, _ in } + manager.fetchQualifiedSegments(userId: userId, options: []) { _, _ in } XCTAssertEqual(segmentManager.receivedUserKey, "fs_user_id") XCTAssertEqual(segmentManager.receivedUserValue, "user-1") - XCTAssertEqual(segmentManager.receivedSegmentsToCheck, []) XCTAssertEqual(segmentManager.receivedOptions, []) } @@ -133,10 +131,10 @@ class OdpManagerTests: XCTestCase { } func testUpdateOdpConfig_flushCalled() { - manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1") + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) XCTAssertTrue(eventManager.flushCalled) - manager.updateOdpConfig(apiKey: nil, apiHost: nil) + manager.updateOdpConfig(apiKey: nil, apiHost: nil, segmentsToCheck: []) XCTAssertTrue(eventManager.flushCalled) } @@ -146,7 +144,7 @@ class OdpManagerTests: XCTestCase { cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeout) - manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1") + manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) XCTAssertEqual(manager.segmentManager?.odpConfig.apiKey, "key-1") XCTAssertEqual(manager.segmentManager?.odpConfig.apiHost, "host-1") @@ -157,7 +155,7 @@ class OdpManagerTests: XCTestCase { // odp disabled with invalid apiKey (apiKey/apiHost propagated into submanagers) - manager.updateOdpConfig(apiKey: nil, apiHost: nil) + manager.updateOdpConfig(apiKey: nil, apiHost: nil, segmentsToCheck: []) XCTAssertEqual(manager.segmentManager?.odpConfig.apiKey, nil) XCTAssertEqual(manager.segmentManager?.odpConfig.apiHost, nil) @@ -209,17 +207,14 @@ class OdpManagerTests: XCTestCase { class MockOdpSegmentManager: OdpSegmentManager { var receivedUserKey: String! var receivedUserValue: String! - var receivedSegmentsToCheck: [String]! var receivedOptions: [OptimizelySegmentOption]! override func fetchQualifiedSegments(userKey: String, userValue: String, - segmentsToCheck: [String], options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { self.receivedUserKey = userKey self.receivedUserValue = userValue - self.receivedSegmentsToCheck = segmentsToCheck self.receivedOptions = options } } diff --git a/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift index dedeca33..db1e05b8 100644 --- a/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift @@ -36,14 +36,13 @@ class OdpSegmentManagerTests: XCTestCase { } func testFetchSegmentsSuccess_cacheMiss() { - odpConfig.update(apiKey: "valid", apiHost: "host") + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["x"]) setCache(userKey, "123", ["a"]) let sem = DispatchSemaphore(value: 0) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, - segmentsToCheck: [], options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["new-customer"], segments) @@ -56,14 +55,13 @@ class OdpSegmentManagerTests: XCTestCase { } func testFetchSegmentsSuccess_cacheHit() { - odpConfig.update(apiKey: "valid", apiHost: "host") + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["x"]) setCache(userKey, userValue, ["a"]) let sem = DispatchSemaphore(value: 0) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, - segmentsToCheck: [], options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["a"], segments) @@ -73,12 +71,11 @@ class OdpSegmentManagerTests: XCTestCase { } func testFetchSegmentsError() { - odpConfig.update(apiKey: "invalid-key", apiHost: "host") + _ = odpConfig.update(apiKey: "invalid-key", apiHost: "host", segmentsToCheck: ["x"]) let sem = DispatchSemaphore(value: 0) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, - segmentsToCheck: [], options: []) { segments, error in XCTAssertNotNil(error) XCTAssertNil(segments) @@ -90,7 +87,7 @@ class OdpSegmentManagerTests: XCTestCase { // MARK: - OptimizelySegmentOption func testOptions_ignoreCache() { - odpConfig.update(apiKey: "valid", apiHost: "host") + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["x"]) setCache(userKey, userValue, ["a"]) options = [.ignoreCache] @@ -98,7 +95,6 @@ class OdpSegmentManagerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, - segmentsToCheck: [], options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["new-customer"], segments, "cache lookup should be skipped") @@ -109,7 +105,7 @@ class OdpSegmentManagerTests: XCTestCase { } func testOptions_resetCache() { - odpConfig.update(apiKey: "valid", apiHost: "host") + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["x"]) setCache(userKey, userValue, ["a"]) setCache(userKey, "123", ["a"]) @@ -119,7 +115,6 @@ class OdpSegmentManagerTests: XCTestCase { let sem = DispatchSemaphore(value: 0) manager.fetchQualifiedSegments(userKey: userKey, userValue: userValue, - segmentsToCheck: [], options: options) { segments, error in XCTAssertNil(error) XCTAssertEqual(["new-customer"], segments, "cache lookup should be skipped") diff --git a/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift b/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift index 266107f9..0a6ecb96 100644 --- a/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpZaiusGraphQLApiManagerTests.swift @@ -258,7 +258,7 @@ extension ZaiusGraphQLApiManagerTests { manager.fetchSegments(apiKey: odpApiKey, apiHost: odpApiHost, userKey: "fs_user_id", - userValue: "not-registered-user", + userValue: "not-registered-user-1", segmentsToCheck: ["segment-1"]) { segments, error in if case .invalidSegmentIdentifier = error { XCTAssert(true) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index 3913c129..3e32aca6 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -66,8 +66,8 @@ class OptimizelyUserContextTests_ODP: XCTestCase { let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments { segments, error in XCTAssertNil(error) - XCTAssert(segments == ["segment-1"]) - XCTAssert(self.user.qualifiedSegments == segments) + XCTAssertEqual(["segment-1"], segments) + XCTAssertEqual(self.user.qualifiedSegments, segments) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -115,11 +115,11 @@ class OptimizelyUserContextTests_ODP: XCTestCase { } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) - XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(odpManager.segmentsToCheck!)) + XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(odpManager.odpConfig.segmentsToCheck)) } func testFetchQualifiedSegments_segmentsNotUsed() { - let datafile = OTUtils.loadJSONDatafile("decide_datafile")! + let datafile = OTUtils.loadJSONDatafile("odp_integrated_no_segments")! try? optimizely.start(datafile: datafile) let sem = DispatchSemaphore(value: 0) @@ -149,7 +149,7 @@ extension OptimizelyUserContextTests_ODP { 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.segmentsToCheck!), "segmentsToCheck should be all-in-project by 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) } @@ -200,6 +200,10 @@ extension OptimizelyUserContextTests_ODP { 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 ZaiusGraphQLApiManagerTests, so skipped here. + func testLiveOdpGraphQL_defaultParameters_userNotRegistered() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) try! optimizely.start(datafile: datafile) @@ -223,7 +227,7 @@ extension OptimizelyUserContextTests_ODP { } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(30))) } - + */ } #endif @@ -232,35 +236,49 @@ extension OptimizelyUserContextTests_ODP { class MockOdpManager: OdpManager { var userId: String? - var segmentsToCheck: [String]! var options: [OptimizelySegmentOption]! var identifyCalled = false - var apiKey: String? - var apiHost: String? + init(sdkKey: String, disable: Bool, cacheSize: Int, cacheTimeoutInSecs: Int) { + super.init(sdkKey: sdkKey, disable: disable, cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeoutInSecs) + self.segmentManager?.zaiusMgr = MockZaiusApiManager() + } override func fetchQualifiedSegments(userId: String, - segmentsToCheck: [String], options: [OptimizelySegmentOption], completionHandler: @escaping ([String]?, OptimizelyError?) -> Void) { self.userId = userId - self.segmentsToCheck = segmentsToCheck self.options = options - - DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - if self.odpConfig.apiKey == nil { - completionHandler(nil, OptimizelyError.generic) - } else { - let sampleSegments = ["segment-1"] - completionHandler(sampleSegments, nil) - } - } + super.fetchQualifiedSegments(userId: userId, options: options, completionHandler: completionHandler) } override func identifyUser(userId: String) { self.userId = userId self.identifyCalled = true } - } +// MARK: - MockZaiusApiManager + +class MockZaiusApiManager: ZaiusGraphQLApiManager { + 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)) { + if apiKey == nil { + completionHandler(nil, OptimizelyError.fetchSegmentsFailed("403")) + } else { + completionHandler(["segment-1"], nil) + } + } + } +} diff --git a/Tests/TestData/decide/decide_audience_segments.json b/Tests/TestData/odp/decide_audience_segments.json similarity index 100% rename from Tests/TestData/decide/decide_audience_segments.json rename to Tests/TestData/odp/decide_audience_segments.json 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" +} From 286990925610b061658c324957de68671bd78a0d Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 3 Aug 2022 16:45:46 -0700 Subject: [PATCH 77/84] fix segmentsToCheck tests --- .../OdpSegmentManagerTests.swift | 13 +++++++------ .../OptimizelyUserContextTests_ODP.swift | 11 ++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift index db1e05b8..7e038669 100644 --- a/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift @@ -36,7 +36,7 @@ class OdpSegmentManagerTests: XCTestCase { } func testFetchSegmentsSuccess_cacheMiss() { - _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["x"]) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["new-customer"]) setCache(userKey, "123", ["a"]) @@ -55,7 +55,7 @@ class OdpSegmentManagerTests: XCTestCase { } func testFetchSegmentsSuccess_cacheHit() { - _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["x"]) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["new-customer"]) setCache(userKey, userValue, ["a"]) @@ -71,7 +71,7 @@ class OdpSegmentManagerTests: XCTestCase { } func testFetchSegmentsError() { - _ = odpConfig.update(apiKey: "invalid-key", apiHost: "host", segmentsToCheck: ["x"]) + _ = odpConfig.update(apiKey: "invalid-key", apiHost: "host", segmentsToCheck: ["new-customer"]) let sem = DispatchSemaphore(value: 0) manager.fetchQualifiedSegments(userKey: userKey, @@ -87,7 +87,7 @@ class OdpSegmentManagerTests: XCTestCase { // MARK: - OptimizelySegmentOption func testOptions_ignoreCache() { - _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["x"]) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["new-customer"]) setCache(userKey, userValue, ["a"]) options = [.ignoreCache] @@ -105,7 +105,7 @@ class OdpSegmentManagerTests: XCTestCase { } func testOptions_resetCache() { - _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["x"]) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: ["new-customer"]) setCache(userKey, userValue, ["a"]) setCache(userKey, "123", ["a"]) @@ -164,7 +164,8 @@ class OdpSegmentManagerTests: XCTestCase { if apiKey == "invalid-key" { completionHandler(nil, OptimizelyError.fetchSegmentsFailed("403")) } else { - completionHandler(["new-customer"], nil) + let qualified = segmentsToCheck.isEmpty ? [] : [segmentsToCheck.first!] + completionHandler(qualified, nil) } } } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index 3e32aca6..bf815c8c 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -66,7 +66,7 @@ class OptimizelyUserContextTests_ODP: XCTestCase { let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments { segments, error in XCTAssertNil(error) - XCTAssertEqual(["segment-1"], segments) + XCTAssertEqual(["odp-segment-1"], segments) XCTAssertEqual(self.user.qualifiedSegments, segments) sem.signal() } @@ -143,7 +143,7 @@ extension OptimizelyUserContextTests_ODP { let sem = DispatchSemaphore(value: 0) user.fetchQualifiedSegments(options: [.ignoreCache]) { segments, error in XCTAssertNil(error) - XCTAssertEqual(segments, ["segment-1"]) + XCTAssertEqual(segments, ["odp-segment-1"]) sem.signal() } XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(3))) @@ -274,11 +274,8 @@ class MockZaiusApiManager: ZaiusGraphQLApiManager { receivedApiHost = apiHost DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - if apiKey == nil { - completionHandler(nil, OptimizelyError.fetchSegmentsFailed("403")) - } else { - completionHandler(["segment-1"], nil) - } + let qualified = segmentsToCheck.isEmpty ? [] : [segmentsToCheck.first!] + completionHandler(qualified, nil) } } } From 4876d07140e5d6034f5de8d6b46eb6ecce80f3ee Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 4 Aug 2022 10:50:26 -0700 Subject: [PATCH 78/84] refact OdpManager --- Sources/ODP/OdpConfig.swift | 49 ++++-- Sources/ODP/OdpEventManager.swift | 10 +- Sources/ODP/OdpManager.swift | 13 +- Sources/ODP/OdpSegmentManager.swift | 4 +- .../OdpEventManagerTests.swift | 140 ++++++++++++------ .../OdpManagerTests.swift | 95 ++++++++++-- 6 files changed, 225 insertions(+), 86 deletions(-) diff --git a/Sources/ODP/OdpConfig.swift b/Sources/ODP/OdpConfig.swift index 61158a94..69083cd5 100644 --- a/Sources/ODP/OdpConfig.swift +++ b/Sources/ODP/OdpConfig.swift @@ -21,32 +21,42 @@ class OdpConfig { private var _apiHost: String? /// The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile. private var _apiKey: String? - /// assumed integrated by default (set to false when datafile has no ODP key/host settings) - private var _odpServiceIntegrated: Bool - /// an array of all ODP segments used in the current datafile (associated with apiHost/apiKey). + /// 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] = [], odpServiceIntegrated: Bool = true) { + init(apiKey: String? = nil, apiHost: String? = nil, segmentsToCheck: [String] = []) { self._apiKey = apiKey self._apiHost = apiHost self._segmentsToCheck = segmentsToCheck - self._odpServiceIntegrated = odpServiceIntegrated // initially assumed true until the first datafile is parsed + self._odpServiceIntegrated = .notDetermined // initially queueing allowed until the first datafile is parsed } func update(apiKey: String?, apiHost: String?, segmentsToCheck: [String]) -> Bool { - // disable future event queueing if datafile has no ODP integrations. - self.odpServiceIntegrated = (apiKey != nil) && (apiHost != nil) + if (apiKey != nil) && (apiHost != nil) { + self.odpServiceIntegrated = OdpConfigState.integrated + } else { + // disable future event queueing if datafile has no ODP integrations. + self.odpServiceIntegrated = OdpConfigState.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 } - - self.apiKey = apiKey - self.apiHost = apiHost - self.segmentsToCheck = segmentsToCheck - return true } } @@ -99,9 +109,9 @@ extension OdpConfig { } } - var odpServiceIntegrated: Bool { + var odpServiceIntegrated: OdpConfigState { get { - var value = false + var value = OdpConfigState.notDetermined queue.sync { value = _odpServiceIntegrated } @@ -113,5 +123,16 @@ extension OdpConfig { } } } + + 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/OdpEventManager.swift b/Sources/ODP/OdpEventManager.swift index 31ff7158..df274f70 100644 --- a/Sources/ODP/OdpEventManager.swift +++ b/Sources/ODP/OdpEventManager.swift @@ -18,8 +18,8 @@ import Foundation import UIKit class OdpEventManager { - let odpConfig: OdpConfig - let zaiusMgr: ZaiusRestApiManager + var odpConfig: OdpConfig + var zaiusMgr: ZaiusRestApiManager let maxQueueSize = 100 let queueLock: DispatchQueue @@ -94,8 +94,8 @@ class OdpEventManager { func dispatch(_ event: OdpEvent) { // do not queue events if datafile has no ODP public key (not integrated) - guard odpConfig.odpServiceIntegrated else { - logger.d("ODP has been disabled.") + guard odpConfig.eventQueueingAllowed else { + logger.d("ODP event has been disabled.") return } @@ -110,7 +110,7 @@ class OdpEventManager { } func flush() { - guard odpConfig.odpServiceIntegrated else { + guard odpConfig.eventQueueingAllowed else { // clean up all pending events if datafile is ready but has no ODP public key (not integrated) _ = eventQueue.removeFirstItems(count: self.maxQueueSize) return diff --git a/Sources/ODP/OdpManager.swift b/Sources/ODP/OdpManager.swift index 17b30bb6..44150f7b 100644 --- a/Sources/ODP/OdpManager.swift +++ b/Sources/ODP/OdpManager.swift @@ -96,18 +96,17 @@ class OdpManager { return } - // flush old events before updating odp integration values + // flush old events using old key before updating odp integration key eventManager?.flush() let configChanged = odpConfig.update(apiKey: apiKey, apiHost: apiHost, segmentsToCheck: segmentsToCheck) + guard configChanged else { return } - if configChanged { - // reset events cache when odp integration or segmentsToCheck changed - segmentManager?.reset() + // reset events cache when odp integration or segmentsToCheck changed + segmentManager?.reset() - // flush old events with the new integration values if events still remain in the queue (when we get the first datafile ready) - eventManager?.flush() - } + // flush events with the new integration key if events still remain in the queue (when we get the first datafile ready) + eventManager?.flush() } } diff --git a/Sources/ODP/OdpSegmentManager.swift b/Sources/ODP/OdpSegmentManager.swift index db098a9d..44c9bf3e 100644 --- a/Sources/ODP/OdpSegmentManager.swift +++ b/Sources/ODP/OdpSegmentManager.swift @@ -17,8 +17,8 @@ import Foundation class OdpSegmentManager { - let odpConfig: OdpConfig - let segmentsCache: LruCache + var odpConfig: OdpConfig + var segmentsCache: LruCache var zaiusMgr: ZaiusGraphQLApiManager let logger = OPTLoggerFactory.getLogger() diff --git a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift index eb5c1374..915df398 100644 --- a/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpEventManagerTests.swift @@ -25,7 +25,7 @@ class OdpEventManagerTests: XCTestCase { 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"] @@ -93,7 +93,7 @@ class OdpEventManagerTests: XCTestCase { func testSendEvent_apiKey() { odpConfig = OdpConfig() - odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager = OdpEventManager(sdkKey: "any", odpConfig: odpConfig, @@ -110,26 +110,70 @@ class OdpEventManagerTests: XCTestCase { // MARK: - flush - func testFlush_apiKey() { - let event = OdpEvent(type: "t1", action: "a1", identifiers: [:], data: [:]) - - // apiKey is not ready + func testFlush_odpIntegrated() { + // apiKey is not ready initially + XCTAssertTrue(manager.odpConfig.eventQueueingAllowed, "initially datafile not ready and assumed queueing is allowed") + + manager.dispatch(event) // each of these will try to flush manager.dispatch(event) manager.dispatch(event) - manager.dispatch(event) - + XCTAssertEqual(3, manager.eventQueue.count) sleep(1) XCTAssertEqual(3, manager.eventQueue.count, "not flushed since apiKey is not ready") - // apiKey is 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 - odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) - manager.flush() 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.dispatch(event) // each of these will try to flush + manager.dispatch(event) + manager.dispatch(event) + + 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: - batch @@ -140,13 +184,13 @@ class OdpEventManagerTests: XCTestCase { ] manager.dispatch(events[0]) - odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) - XCTAssertEqual(1, apiManager.receivedBatchEvents.count) - XCTAssertEqual(1, apiManager.receivedBatchEvents[0].count) - validateEvents(events, apiManager.receivedBatchEvents[0]) + XCTAssertEqual(1, apiManager.dispatchedBatchEvents.count) + XCTAssertEqual(1, apiManager.dispatchedBatchEvents[0].count) + validateEvents(events, apiManager.dispatchedBatchEvents[0]) } func testFlush_batch_3() { @@ -160,13 +204,13 @@ class OdpEventManagerTests: XCTestCase { manager.dispatch(e) } - odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) - XCTAssertEqual(1, apiManager.receivedBatchEvents.count) - XCTAssertEqual(3, apiManager.receivedBatchEvents[0].count) - validateEvents(events, apiManager.receivedBatchEvents[0]) + XCTAssertEqual(1, apiManager.dispatchedBatchEvents.count) + XCTAssertEqual(3, apiManager.dispatchedBatchEvents[0].count) + validateEvents(events, apiManager.dispatchedBatchEvents[0]) } func testFlush_batch_moreThanBatchSize() { @@ -177,22 +221,22 @@ class OdpEventManagerTests: XCTestCase { manager.dispatch(e) } - odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) - XCTAssertEqual(2, apiManager.receivedBatchEvents.count) - XCTAssertEqual(10, apiManager.receivedBatchEvents[0].count) - XCTAssertEqual(1, apiManager.receivedBatchEvents[1].count) - validateEvents(events, apiManager.receivedBatchEvents[0] + apiManager.receivedBatchEvents[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: []) + _ = odpConfig.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) - XCTAssertEqual(0, apiManager.receivedBatchEvents.count) + XCTAssertEqual(0, apiManager.dispatchedBatchEvents.count) } // MARK: - multiple skdKeys @@ -218,25 +262,25 @@ class OdpEventManagerTests: XCTestCase { manager2.dispatch(event2) - XCTAssertEqual(0, apiManager1.receivedBatchEvents.count) - XCTAssertEqual(0, apiManager2.receivedBatchEvents.count) + XCTAssertEqual(0, apiManager1.dispatchedBatchEvents.count) + XCTAssertEqual(0, apiManager2.dispatchedBatchEvents.count) - odpConfig1.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + _ = odpConfig1.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager1.flush() sleep(1) - XCTAssertEqual(1, apiManager1.receivedBatchEvents.count) - XCTAssertEqual(2, apiManager1.receivedBatchEvents[0].count) - XCTAssertEqual(0, apiManager2.receivedBatchEvents.count) + XCTAssertEqual(1, apiManager1.dispatchedBatchEvents.count) + XCTAssertEqual(2, apiManager1.dispatchedBatchEvents[0].count) + XCTAssertEqual(0, apiManager2.dispatchedBatchEvents.count) - odpConfig2.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) + _ = odpConfig2.update(apiKey: "valid", apiHost: "host", segmentsToCheck: []) manager2.flush() sleep(1) - XCTAssertEqual(1, apiManager1.receivedBatchEvents.count) - XCTAssertEqual(2, apiManager1.receivedBatchEvents[0].count) - XCTAssertEqual(1, apiManager2.receivedBatchEvents.count) - XCTAssertEqual(1, apiManager2.receivedBatchEvents[0].count) + XCTAssertEqual(1, apiManager1.dispatchedBatchEvents.count) + XCTAssertEqual(2, apiManager1.dispatchedBatchEvents[0].count) + XCTAssertEqual(1, apiManager2.dispatchedBatchEvents.count) + XCTAssertEqual(1, apiManager2.dispatchedBatchEvents[0].count) } // MARK: - errors @@ -249,11 +293,11 @@ class OdpEventManagerTests: XCTestCase { manager.dispatch(e) } - odpConfig.update(apiKey: "valid-key-retry-error", apiHost: "host", segmentsToCheck: []) + _ = odpConfig.update(apiKey: "valid-key-retry-error", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) - XCTAssertEqual(1, apiManager.receivedBatchEvents.count, "should be not retried immediately (a batch of 2 events)") + 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") } @@ -265,20 +309,20 @@ class OdpEventManagerTests: XCTestCase { manager.dispatch(e) } - odpConfig.update(apiKey: "invalid-key-no-retry", apiHost: "host", segmentsToCheck: []) + _ = odpConfig.update(apiKey: "invalid-key-no-retry", apiHost: "host", segmentsToCheck: []) manager.flush() sleep(1) - XCTAssertEqual(2, apiManager.receivedBatchEvents.count, "should not be retried (only once for each of batch events)") - XCTAssertEqual(10, apiManager.receivedBatchEvents[0].count) - XCTAssertEqual(5, apiManager.receivedBatchEvents[1].count) + 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") } // MARK: - OdpConfig func testOdpConfig() { - odpConfig.update(apiKey: "test-key", apiHost: "test-host", segmentsToCheck: []) + _ = odpConfig.update(apiKey: "test-key", apiHost: "test-host", segmentsToCheck: []) manager = OdpEventManager(sdkKey: "any", odpConfig: odpConfig, @@ -352,7 +396,11 @@ class OdpEventManagerTests: XCTestCase { class MockZaiusApiManager: ZaiusRestApiManager { var receivedApiKey: String! var receivedApiHost: String! - var receivedBatchEvents = [[OdpEvent]]() + var dispatchedBatchEvents = [[OdpEvent]]() + + var totalDispatchedEvents: Int { + return dispatchedBatchEvents.reduce(0) { $0 + $1.count } + } override func sendOdpEvents(apiKey: String, apiHost: String, @@ -360,7 +408,7 @@ class OdpEventManagerTests: XCTestCase { completionHandler: @escaping (OptimizelyError?) -> Void) { receivedApiKey = apiKey receivedApiHost = apiHost - receivedBatchEvents.append(events) + dispatchedBatchEvents.append(events) DispatchQueue.global().async { if apiKey == "invalid-key-no-retry" { diff --git a/Tests/OptimizelyTests-Common/OdpManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift index acbfb103..0f67c9f9 100644 --- a/Tests/OptimizelyTests-Common/OdpManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -18,7 +18,6 @@ import XCTest class OdpManagerTests: XCTestCase { let sdkKey = "any" - let odpConfig = OdpConfig() let cacheSize = 10 let cacheTimeout = 20 var segmentManager: MockOdpSegmentManager! @@ -29,14 +28,16 @@ class OdpManagerTests: XCTestCase { OTUtils.clearAllEventQueues() segmentManager = MockOdpSegmentManager(cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeout, - odpConfig: odpConfig) - eventManager = MockOdpEventManager(sdkKey: sdkKey, odpConfig: odpConfig) + odpConfig: OdpConfig()) + eventManager = MockOdpEventManager(sdkKey: sdkKey, odpConfig: OdpConfig()) manager = OdpManager(sdkKey: sdkKey, disable: false, cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeout, segmentManager: segmentManager, eventManager: eventManager) + segmentManager.odpConfig = manager.odpConfig + eventManager.odpConfig = manager.odpConfig } override func tearDown() { @@ -130,12 +131,76 @@ class OdpManagerTests: XCTestCase { XCTAssertEqual(eventManager.receivedIdentifiers, ["vuid": "vuid-fixed", "id-key1": "id-val-1"]) } + func testUpdateOdpConfig_resetCalled() { + 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() { manager.updateOdpConfig(apiKey: "key-1", apiHost: "host-1", segmentsToCheck: []) - XCTAssertTrue(eventManager.flushCalled) - + XCTAssertEqual(eventManager.flushApiKeys.count, 2, "flush called before and after update") + XCTAssertEqual(eventManager.flushApiKeys[0], nil) + XCTAssertEqual(eventManager.flushApiKeys[1], "key-1") + + eventManager.flushApiKeys.removeAll() + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-1", segmentsToCheck: []) + XCTAssertEqual(eventManager.flushApiKeys.count, 2) + XCTAssertEqual(eventManager.flushApiKeys[0], "key-1") + XCTAssertEqual(eventManager.flushApiKeys[1], "key-2") + + eventManager.flushApiKeys.removeAll() + + manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-1", segmentsToCheck: []) + XCTAssertEqual(eventManager.flushApiKeys.count, 1, "flush called once when no change") + XCTAssertEqual(eventManager.flushApiKeys[0], "key-2") + + eventManager.flushApiKeys.removeAll() + manager.updateOdpConfig(apiKey: nil, apiHost: nil, segmentsToCheck: []) - XCTAssertTrue(eventManager.flushCalled) + XCTAssertEqual(eventManager.flushApiKeys.count, 2) + XCTAssertEqual(eventManager.flushApiKeys[0], "key-2") + XCTAssertEqual(eventManager.flushApiKeys[1], nil) } func testUpdateOdpConfig_odpConfigPropagatedProperly() { @@ -148,10 +213,10 @@ class OdpManagerTests: XCTestCase { XCTAssertEqual(manager.segmentManager?.odpConfig.apiKey, "key-1") XCTAssertEqual(manager.segmentManager?.odpConfig.apiHost, "host-1") - XCTAssertEqual(manager.segmentManager?.odpConfig.odpServiceIntegrated, true) + XCTAssertEqual(manager.segmentManager?.odpConfig.eventQueueingAllowed, true) XCTAssertEqual(manager.eventManager?.odpConfig.apiKey, "key-1") XCTAssertEqual(manager.eventManager?.odpConfig.apiHost, "host-1") - XCTAssertEqual(manager.eventManager?.odpConfig.odpServiceIntegrated, true) + XCTAssertEqual(manager.eventManager?.odpConfig.eventQueueingAllowed, true) // odp disabled with invalid apiKey (apiKey/apiHost propagated into submanagers) @@ -159,10 +224,10 @@ class OdpManagerTests: XCTestCase { XCTAssertEqual(manager.segmentManager?.odpConfig.apiKey, nil) XCTAssertEqual(manager.segmentManager?.odpConfig.apiHost, nil) - XCTAssertEqual(manager.segmentManager?.odpConfig.odpServiceIntegrated, false) + XCTAssertEqual(manager.segmentManager?.odpConfig.eventQueueingAllowed, false) XCTAssertEqual(manager.eventManager?.odpConfig.apiKey, nil) XCTAssertEqual(manager.eventManager?.odpConfig.apiHost, nil) - XCTAssertEqual(manager.eventManager?.odpConfig.odpServiceIntegrated, false) + XCTAssertEqual(manager.eventManager?.odpConfig.eventQueueingAllowed, false) } @@ -181,7 +246,7 @@ class OdpManagerTests: XCTestCase { var receivedIdentifiers: [String: String]! var receivedData: [String: Any]! - var flushCalled = false + var flushApiKeys = [String?]() override func registerVUID(vuid: String) { self.receivedVuid = vuid @@ -200,7 +265,7 @@ class OdpManagerTests: XCTestCase { } override func flush() { - self.flushCalled = true + self.flushApiKeys.append(odpConfig.apiKey) } } @@ -209,6 +274,8 @@ class OdpManagerTests: XCTestCase { var receivedUserValue: String! var receivedOptions: [OptimizelySegmentOption]! + var resetCalled = false + override func fetchQualifiedSegments(userKey: String, userValue: String, options: [OptimizelySegmentOption], @@ -217,6 +284,10 @@ class OdpManagerTests: XCTestCase { self.receivedUserValue = userValue self.receivedOptions = options } + + override func reset() { + self.resetCalled = true + } } } From 8ef2383d032f5d7d52bab7f75d88f07f3264ddad Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 4 Aug 2022 11:28:56 -0700 Subject: [PATCH 79/84] fix tests --- Sources/ODP/OdpManager.swift | 7 +++++-- Tests/OptimizelyTests-Common/OdpManagerTests.swift | 4 ++-- Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift | 3 +-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/ODP/OdpManager.swift b/Sources/ODP/OdpManager.swift index 44150f7b..0015bb55 100644 --- a/Sources/ODP/OdpManager.swift +++ b/Sources/ODP/OdpManager.swift @@ -96,13 +96,16 @@ class OdpManager { return } - // flush old events using old key before updating odp integration key + // 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 may be dispatched with the new odp key later. eventManager?.flush() let configChanged = odpConfig.update(apiKey: apiKey, apiHost: apiHost, segmentsToCheck: segmentsToCheck) guard configChanged else { return } - // reset events cache when odp integration or segmentsToCheck changed + // reset segments cache when odp integration or segmentsToCheck are changed segmentManager?.reset() // flush events with the new integration key if events still remain in the queue (when we get the first datafile ready) diff --git a/Tests/OptimizelyTests-Common/OdpManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift index 0f67c9f9..05cfa546 100644 --- a/Tests/OptimizelyTests-Common/OdpManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -186,8 +186,8 @@ class OdpManagerTests: XCTestCase { manager.updateOdpConfig(apiKey: "key-2", apiHost: "host-1", segmentsToCheck: []) XCTAssertEqual(eventManager.flushApiKeys.count, 2) - XCTAssertEqual(eventManager.flushApiKeys[0], "key-1") - XCTAssertEqual(eventManager.flushApiKeys[1], "key-2") + XCTAssertEqual(eventManager.flushApiKeys[0], "key-1", "old events must be flushed with the old odp key") + XCTAssertEqual(eventManager.flushApiKeys[1], "key-2", "remaining events must be flushed with the new odp key") eventManager.flushApiKeys.removeAll() diff --git a/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift index 7e038669..3955d27b 100644 --- a/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpSegmentManagerTests.swift @@ -164,8 +164,7 @@ class OdpSegmentManagerTests: XCTestCase { if apiKey == "invalid-key" { completionHandler(nil, OptimizelyError.fetchSegmentsFailed("403")) } else { - let qualified = segmentsToCheck.isEmpty ? [] : [segmentsToCheck.first!] - completionHandler(qualified, nil) + completionHandler(segmentsToCheck, nil) } } } From 2d746db20b7149c32949bd97a9fd0cab858d54de Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 4 Aug 2022 11:44:52 -0700 Subject: [PATCH 80/84] clean up odpConfig link --- Sources/ODP/OdpManager.swift | 19 +++++++++++++++---- .../OdpManagerTests.swift | 2 -- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Sources/ODP/OdpManager.swift b/Sources/ODP/OdpManager.swift index 0015bb55..ea17398e 100644 --- a/Sources/ODP/OdpManager.swift +++ b/Sources/ODP/OdpManager.swift @@ -42,10 +42,21 @@ class OdpManager { self.vuidManager = OdpVuidManager.shared if enabled { - self.segmentManager = segmentManager ?? OdpSegmentManager(cacheSize: cacheSize, - cacheTimeoutInSecs: cacheTimeoutInSecs, - odpConfig: odpConfig) - self.eventManager = eventManager ?? OdpEventManager(sdkKey: sdkKey, 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) } diff --git a/Tests/OptimizelyTests-Common/OdpManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift index 05cfa546..3bbe4664 100644 --- a/Tests/OptimizelyTests-Common/OdpManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -36,8 +36,6 @@ class OdpManagerTests: XCTestCase { cacheTimeoutInSecs: cacheTimeout, segmentManager: segmentManager, eventManager: eventManager) - segmentManager.odpConfig = manager.odpConfig - eventManager.odpConfig = manager.odpConfig } override func tearDown() { From e1b69243030d4e44b45dcffa81c2ebfbcc55d74e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 4 Aug 2022 14:52:47 -0700 Subject: [PATCH 81/84] refact updateOdpConfig --- Sources/Optimizely/OptimizelyClient.swift | 14 ++-- .../OptimizelyClientTests_ODP.swift | 84 ++++--------------- 2 files changed, 25 insertions(+), 73 deletions(-) diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index 9d98f2e0..6b72ae54 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -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) + } } } @@ -199,13 +205,7 @@ open class OptimizelyClient: NSObject { func configSDK(datafile: Data) throws { do { self.config = try ProjectConfig(datafile: datafile) - - guard let config = self.config else { throw OptimizelyError.dataFileInvalid } - - odpManager.updateOdpConfig(apiKey: config.publicKeyForODP, - apiHost: config.hostForODP, - segmentsToCheck: config.allSegments) - + datafileHandler?.startUpdates(sdkKey: self.sdkKey) { data in // new datafile came in self.updateConfigFromBackgroundFetch(data: data) diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift index 313bb5bc..ac93e661 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift @@ -76,73 +76,39 @@ class OptimizelyClientTests_ODP: XCTestCase { // MARK: - OdpConfig Update - func testUpdateOpdConfigCalled_synchronous_success() { + 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 - let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! - try? optimizely.start(datafile: 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) - } - - func testUpdateOpdConfigCalled_synchronous_failure() { - let odpManager = MockOdpManager(sdkKey: "any", disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) - optimizely.odpManager = odpManager - - XCTAssertNil(odpManager.apiKey) - XCTAssertNil(odpManager.apiHost) + XCTAssertEqual(["odp-segment-1", "odp-segment-2", "odp-segment-3"], odpManager.segmentsToCheck.sorted()) - let datafile = OTUtils.loadJSONDatafile("unsupported_datafile")! - try? optimizely.start(datafile: datafile) + // ODP not integrated in datafile - XCTAssertNil(odpManager.apiKey, "updateOdpConfig should not be called when datafile parse failed") + 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) } - func testUpdateOpdConfigCalled_asynchronous_success() { - let sdkKey = "valid" - let optimizely = OptimizelyClient(sdkKey: sdkKey, datafileHandler: MockDatafileHandler()) - let odpManager = MockOdpManager(sdkKey: sdkKey, disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) - optimizely.odpManager = odpManager - - XCTAssertNil(odpManager.apiKey) - XCTAssertNil(odpManager.apiHost) - - let sem = DispatchSemaphore(value: 0) - optimizely.start { result in - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - - XCTAssertEqual("W4WzcEs-ABgXorzY7h1LCQ", odpManager.apiKey, "updateOdpConfig should be called when datafile fetched") - XCTAssertEqual("https://api.zaius.com", odpManager.apiHost) - } + // MARK: - Utils - func testUpdateOpdConfigCalled_asynchronous_failure() { - let sdkKey = "invalid" - let optimizely = OptimizelyClient(sdkKey: sdkKey, datafileHandler: MockDatafileHandler()) - let odpManager = MockOdpManager(sdkKey: sdkKey, disable: false, cacheSize: 12, cacheTimeoutInSecs: 123) - optimizely.odpManager = odpManager - - XCTAssertNil(odpManager.apiKey) - XCTAssertNil(odpManager.apiHost) - - let sem = DispatchSemaphore(value: 0) - optimizely.start { result in - sem.signal() - } - XCTAssertEqual(.success, sem.wait(timeout: .now() + .seconds(1))) - - XCTAssertNil(odpManager.apiKey, "updateOdpConfig should not be called on datafile fetch failed") - XCTAssertNil(odpManager.apiHost) + func updateProjectConfig(optimizely: OptimizelyClient, datafile: Data) { + optimizely.config = try! ProjectConfig(datafile: datafile) } - } // MARK: - Mocks @@ -157,6 +123,7 @@ extension OptimizelyClientTests_ODP { 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 @@ -168,23 +135,8 @@ extension OptimizelyClientTests_ODP { override func updateOdpConfig(apiKey: String?, apiHost: String?, segmentsToCheck: [String]) { self.apiKey = apiKey self.apiHost = apiHost + self.segmentsToCheck = segmentsToCheck } } - class MockDatafileHandler: DefaultDatafileHandler { - - override func downloadDatafile(sdkKey: String, - returnCacheIfNoChange: Bool, - resourceTimeoutInterval: Double?, - completionHandler: @escaping DatafileDownloadCompletionHandler) { - if sdkKey == "valid" { - let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! - completionHandler(.success(datafile)) - } else { - completionHandler(.failure(.generic)) - } - } - - } - } From 3bb769fabd7cfe8d521d02b758e1e42080105555 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 4 Aug 2022 15:22:56 -0700 Subject: [PATCH 82/84] add reset test for lruCache --- Sources/ODP/LruCache.swift | 14 +++--- .../OptimizelyClientTests_ODP.swift | 4 +- .../LruCacheTests.swift | 47 +++++++++++++++++-- .../OdpManagerTests.swift | 2 +- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/Sources/ODP/LruCache.swift b/Sources/ODP/LruCache.swift index f411245a..a6b9fb1a 100644 --- a/Sources/ODP/LruCache.swift +++ b/Sources/ODP/LruCache.swift @@ -36,17 +36,17 @@ class LruCache { var head: CacheElement! var tail: CacheElement! let queue = DispatchQueue(label: "LRU") - let size: Int + let maxSize: Int let timeoutInSecs: Int init(size: Int, timeoutInSecs: Int) { - self.size = size + self.maxSize = size self.timeoutInSecs = timeoutInSecs self.reset() } func lookup(key: K) -> V? { - if size <= 0 { return nil } + if maxSize <= 0 { return nil } var element: CacheElement? = nil @@ -69,7 +69,7 @@ class LruCache { } func save(key: K, value: V) { - if size <= 0 { return } + if maxSize <= 0 { return } queue.async(flags: .barrier) { let oldSegments = self.map[key] @@ -81,7 +81,7 @@ class LruCache { } self.addToLink(newSegments) - while self.map.count > self.size { + 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 @@ -91,7 +91,7 @@ class LruCache { // read cache contents without order update func peek(key: K) -> V? { - if size <= 0 { return nil } + if maxSize <= 0 { return nil } var element: CacheElement? = nil queue.sync { @@ -101,7 +101,7 @@ class LruCache { } func reset() { - if size <= 0 { return } + if maxSize <= 0 { return } queue.sync { map = [K: CacheElement]() diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift index ac93e661..ecf6de2d 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift @@ -33,7 +33,7 @@ class OptimizelyClientTests_ODP: XCTestCase { func testConfigurableSettings_default() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - XCTAssertEqual(100, optimizely.odpManager.segmentManager?.segmentsCache.size) + XCTAssertEqual(100, optimizely.odpManager.segmentManager?.segmentsCache.maxSize) XCTAssertEqual(600, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) XCTAssertEqual(true, optimizely.odpManager.enabled) } @@ -41,7 +41,7 @@ class OptimizelyClientTests_ODP: XCTestCase { func testConfigurableSettings_custom() { var sdkSettings = OptimizelySdkSettings(segmentsCacheSize: 12, segmentsCacheTimeoutInSecs: 345) var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) - XCTAssertEqual(12, optimizely.odpManager.segmentManager?.segmentsCache.size) + XCTAssertEqual(12, optimizely.odpManager.segmentManager?.segmentsCache.maxSize) XCTAssertEqual(345, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) sdkSettings = OptimizelySdkSettings(disableOdp: true) diff --git a/Tests/OptimizelyTests-Common/LruCacheTests.swift b/Tests/OptimizelyTests-Common/LruCacheTests.swift index 4bc0ba23..096827c7 100644 --- a/Tests/OptimizelyTests-Common/LruCacheTests.swift +++ b/Tests/OptimizelyTests-Common/LruCacheTests.swift @@ -20,11 +20,11 @@ class LruCacheTests: XCTestCase { func testMinConfig() { var cache = LruCache(size: 1000, timeoutInSecs: 2000) - XCTAssertEqual(1000, cache.size) + XCTAssertEqual(1000, cache.maxSize) XCTAssertEqual(2000, cache.timeoutInSecs) cache = LruCache(size: 0, timeoutInSecs: 0) - XCTAssertEqual(0, cache.size) + XCTAssertEqual(0, cache.maxSize) XCTAssertEqual(0, cache.timeoutInSecs) } @@ -66,15 +66,56 @@ class LruCacheTests: XCTestCase { node = node?.next } XCTAssertEqual(maxSize, count - 2) // subtract 2 (head, tail) - XCTAssertEqual(cache.map.count, cache.size) + 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)) } } diff --git a/Tests/OptimizelyTests-Common/OdpManagerTests.swift b/Tests/OptimizelyTests-Common/OdpManagerTests.swift index 3bbe4664..a412fa11 100644 --- a/Tests/OptimizelyTests-Common/OdpManagerTests.swift +++ b/Tests/OptimizelyTests-Common/OdpManagerTests.swift @@ -49,7 +49,7 @@ class OdpManagerTests: XCTestCase { disable: false, cacheSize: cacheSize, cacheTimeoutInSecs: cacheTimeout) - XCTAssertEqual(manager.segmentManager?.segmentsCache.size, cacheSize) + XCTAssertEqual(manager.segmentManager?.segmentsCache.maxSize, cacheSize) XCTAssertEqual(manager.segmentManager?.segmentsCache.timeoutInSecs, cacheTimeout) } From d8f4d1055ee740c931c8766703ac907fc35ce8c0 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 10 Aug 2022 09:34:30 -0700 Subject: [PATCH 83/84] filtering out redundant segments --- Sources/Data Model/ProjectConfig.swift | 2 +- .../ProjectConfigTests.swift | 2 +- .../odp/decide_audience_segments.json | 23 +++++++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/Data Model/ProjectConfig.swift b/Sources/Data Model/ProjectConfig.swift index 2330f432..c1faa357 100644 --- a/Sources/Data Model/ProjectConfig.swift +++ b/Sources/Data Model/ProjectConfig.swift @@ -150,7 +150,7 @@ class ProjectConfig { self.allSegments = { let audiences = project.typedAudiences ?? [] - return audiences.flatMap { $0.getSegments() } + return Array(Set(audiences.flatMap { $0.getSegments() })) }() } diff --git a/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift b/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift index a9c47270..1ad8c2c0 100644 --- a/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift +++ b/Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift @@ -103,7 +103,7 @@ class ProjectConfigTests: XCTestCase { userProfileService: OTUtils.createClearUserProfileService()) try! optimizely.start(datafile: datafile) let segments = optimizely.config!.allSegments - XCTAssertEqual(3, segments.count) + XCTAssertEqual(3, segments.count, "redundant items should be filtered out") XCTAssertEqual(Set(["odp-segment-1", "odp-segment-2", "odp-segment-3"]), Set(segments)) } diff --git a/Tests/TestData/odp/decide_audience_segments.json b/Tests/TestData/odp/decide_audience_segments.json index 4427915c..ed43f4d8 100644 --- a/Tests/TestData/odp/decide_audience_segments.json +++ b/Tests/TestData/odp/decide_audience_segments.json @@ -128,7 +128,7 @@ ] ] ], - "name": "odp-segment-1" + "name": "odp-audience-1" }, { "id": "13389130056", @@ -162,7 +162,26 @@ ] ] ], - "name": "odp-segment-2" + "name": "odp-audience-2" + }, + { + "id": "13389130056", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-audience-3" } ], "audiences": [ From 622832fe68f96ebf33deae3c384bad3563d90bd3 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 10 Aug 2022 09:45:46 -0700 Subject: [PATCH 84/84] clean up tests --- .../OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift index bf815c8c..44f2d5c2 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_ODP.swift @@ -274,7 +274,7 @@ class MockZaiusApiManager: ZaiusGraphQLApiManager { receivedApiHost = apiHost DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - let qualified = segmentsToCheck.isEmpty ? [] : [segmentsToCheck.first!] + let qualified = segmentsToCheck.isEmpty ? [] : [segmentsToCheck.sorted{ $0 < $1 }.first!] completionHandler(qualified, nil) } }