diff --git a/Split.xcodeproj/project.pbxproj b/Split.xcodeproj/project.pbxproj index 3d7b9fe2f..7068190d0 100644 --- a/Split.xcodeproj/project.pbxproj +++ b/Split.xcodeproj/project.pbxproj @@ -353,6 +353,9 @@ 59FB7C35220329B900ECC96A /* SplitFactoryBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C34220329B900ECC96A /* SplitFactoryBuilderTests.swift */; }; 59FB7C3C2203795F00ECC96A /* LocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */; }; 59FB7C3E22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */; }; + 5B279CF92E340FC600B73A36 /* splitschanges_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 5B279CF82E340FB900B73A36 /* splitschanges_no_segments.json */; }; + 5B343EAD2E26E93B006BEBE7 /* StorageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */; }; + 5B343EAE2E26E93B006BEBE7 /* StorageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */; }; 5B48D8172DEA2CED00351925 /* PrerequisitesMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */; }; 5B91B8392DDE4A3B000510F0 /* SplitDTOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B91B8382DDE4A30000510F0 /* SplitDTOTests.swift */; }; 5BF52DF72DE0B60700FEDAFE /* PrerequisitesMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */; }; @@ -1557,6 +1560,8 @@ 59FB7C34220329B900ECC96A /* SplitFactoryBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitFactoryBuilderTests.swift; sourceTree = ""; }; 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalhostSplitsParser.swift; sourceTree = ""; }; 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceDelimitedLocalhostSplitsParser.swift; sourceTree = ""; }; + 5B279CF82E340FB900B73A36 /* splitschanges_no_segments.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = splitschanges_no_segments.json; sourceTree = ""; }; + 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageHelper.swift; sourceTree = ""; }; 5B91B8382DDE4A30000510F0 /* SplitDTOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitDTOTests.swift; sourceTree = ""; }; 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrerequisitesMatcher.swift; sourceTree = ""; }; 5BF52DF82DE4B8CA00FEDAFE /* PrerequisitesMatcherTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrerequisitesMatcherTest.swift; sourceTree = ""; }; @@ -2620,6 +2625,7 @@ 5905D4DD2559C950006DA3B1 /* MySegments */, 3B6DEF0920EA6AE40067435E /* DefaultFileStorage.swift */, 5912D1462194715500BC698C /* FileStorage.swift */, + 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */, 59E6A38825557C8D005DE642 /* split_cache.xcdatamodeld */, 5905D4D72555FE74006DA3B1 /* SplitDatabase.swift */, 955892AA25C187EA00F67FBA /* CoreDataContextBuilder.swift */, @@ -2939,6 +2945,7 @@ 592C6ADB211CBB86002D120C /* Resources */ = { isa = PBXGroup; children = ( + 5B279CF82E340FB900B73A36 /* splitschanges_no_segments.json */, C5A7D5562DDBD4280081D190 /* split_changes_rbs.json */, 95F7BBD32C1A273900C5F2E4 /* Cert */, C5977C052BF273A3003E293A /* between_semver.csv */, @@ -3987,6 +3994,7 @@ 95F7BBDE2C1A27C200C5F2E4 /* rsa_3072_public_01.pem in Resources */, C5BD1E522D130FB6008EF198 /* splitchanges_toggle.json in Resources */, 95F7BBDA2C1A27C200C5F2E4 /* rsa_2048_public_01.pem in Resources */, + 5B279CF92E340FC600B73A36 /* splitschanges_no_segments.json in Resources */, 592C6AED211CBBB0002D120C /* splitchanges_2.json in Resources */, 950F72FF292E8D6C008A0040 /* SplitiOSFull.xctestplan in Resources */, 59FB7C2A2202463000ECC96A /* localhost_1.splits in Resources */, @@ -4241,6 +4249,7 @@ 59F4AA9B24FE93E300A1C69A /* NotificationManagerKeeper.swift in Sources */, 95C1600D27D28CF4008562E3 /* PersistentAttributesStorage.swift in Sources */, 95726075262F548500350CCA /* SplitBgSynchronizer.swift in Sources */, + 5B343EAE2E26E93B006BEBE7 /* StorageHelper.swift in Sources */, 95C1600B27D28CB8008562E3 /* OneKeyPersistentAttributesStorage.swift in Sources */, 5BF52DF72DE0B60700FEDAFE /* PrerequisitesMatcher.swift in Sources */, 9519A91127D6935700278AEC /* ByKeyAttributesStorage.swift in Sources */, @@ -4802,6 +4811,7 @@ 958FD8A12C51318B00E5609B /* PublicKeyHeaders.swift in Sources */, 956A7E17297043130080D53C /* ImpressionsStorage.swift in Sources */, 95B02DCD28D0BDE20030EC8B /* split_cache.xcdatamodeld in Sources */, + 5B343EAD2E26E93B006BEBE7 /* StorageHelper.swift in Sources */, 95F0569129B63C61009F5A68 /* Cipher.swift in Sources */, 95B02CAB28D0BDC10030EC8B /* Yaml.swift in Sources */, 95B02CAC28D0BDC10030EC8B /* YAMLOperators.swift in Sources */, diff --git a/Split/Api/SplitDatabaseHelper.swift b/Split/Api/SplitDatabaseHelper.swift index 953272bcc..12eb27e9c 100644 --- a/Split/Api/SplitDatabaseHelper.swift +++ b/Split/Api/SplitDatabaseHelper.swift @@ -111,9 +111,11 @@ struct SplitDatabaseHelper { let persistentEventsStorage = openPersistentEventsStorage(database: splitDatabase) let eventsStorage = openEventsStorage(persistentStorage: persistentEventsStorage) + + let generalInfoStorage = openGeneralInfoStorage(database: splitDatabase) - let mySegmentsStorage = openMySegmentsStorage(database: splitDatabase) - let myLargeSegmentsStorage = openMyLargeSegmentsStorage(database: splitDatabase) + let mySegmentsStorage = openMySegmentsStorage(database: splitDatabase, generalInfoStorage: generalInfoStorage) + let myLargeSegmentsStorage = openMyLargeSegmentsStorage(database: splitDatabase, generalInfoStorage: generalInfoStorage) let attributesStorage = openAttributesStorage(database: splitDatabase, splitClientConfig: splitClientConfig) @@ -125,7 +127,6 @@ struct SplitDatabaseHelper { let hashedImpressionsStorage = DefaultHashedImpressionsStorage( cache: LRUCache(capacity: ServiceConstants.lastSeenImpressionCachSize), persistentStorage: persistentHashedImpressionsStorage) - let generalInfoStorage = openGeneralInfoStorage(database: splitDatabase) let persistentRuleBasedSegmentsStorage = DefaultPersistentRuleBasedSegmentsStorage( database: splitDatabase, @@ -187,14 +188,14 @@ struct SplitDatabaseHelper { return DefaultPersistentMyLargeSegmentsStorage(database: database) } - static func openMySegmentsStorage(database: SplitDatabase) -> MySegmentsStorage { + static func openMySegmentsStorage(database: SplitDatabase, generalInfoStorage: GeneralInfoStorage) -> MySegmentsStorage { let persistentMySegmentsStorage = openPersistentMySegmentsStorage(database: database) - return DefaultMySegmentsStorage(persistentMySegmentsStorage: persistentMySegmentsStorage) + return DefaultMySegmentsStorage(persistentMySegmentsStorage: persistentMySegmentsStorage, generalInfoStorage: generalInfoStorage) } - static func openMyLargeSegmentsStorage(database: SplitDatabase) -> MySegmentsStorage { + static func openMyLargeSegmentsStorage(database: SplitDatabase, generalInfoStorage: GeneralInfoStorage) -> MySegmentsStorage { let persistentMyLargeSegmentsStorage = openPersistentMyLargeSegmentsStorage(database: database) - return MyLargeSegmentsStorage(persistentStorage: persistentMyLargeSegmentsStorage) + return MyLargeSegmentsStorage(persistentStorage: persistentMyLargeSegmentsStorage, generalInfoStorage: generalInfoStorage) } static func openPersistentAttributesStorage(database: SplitDatabase) -> PersistentAttributesStorage { diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index ea64d0279..a84cbdb37 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -102,7 +102,6 @@ class DefaultSplitEventsManager: SplitEventsManager { self.eventsQueue.stop() self.eventsQueue.stop() } - } } diff --git a/Split/FetcherEngine/Refresh/EmptyMySegmentsStorage.swift b/Split/FetcherEngine/Refresh/EmptyMySegmentsStorage.swift index 1ca633b9e..a11a00bcd 100644 --- a/Split/FetcherEngine/Refresh/EmptyMySegmentsStorage.swift +++ b/Split/FetcherEngine/Refresh/EmptyMySegmentsStorage.swift @@ -47,4 +47,8 @@ class EmptyMySegmentsStorage: MySegmentsStorage { func clear() { } + + func isUsingSegments() -> Bool { + false + } } diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index b6fd71c81..c2693d298 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -213,8 +213,9 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { } override func fetchFromRemote() { - // Polling should be done once sdk ready is fired in initial sync - if !isSdkReadyFired() { + // Polling should be done once sdk ready is fired in initial sync, and if there are Segments in use. + // Both storages read the same value so we can use any of them (using myLargeSegmentsStorage). + if !isSdkReadyFired() || !(myLargeSegmentsStorage.isUsingSegments()) { return } diff --git a/Split/Localhost/LocalhostSplitsStorage.swift b/Split/Localhost/LocalhostSplitsStorage.swift index 11076558e..707aeeb4a 100644 --- a/Split/Localhost/LocalhostSplitsStorage.swift +++ b/Split/Localhost/LocalhostSplitsStorage.swift @@ -14,6 +14,7 @@ class LocalhostSplitsStorage: SplitsStorage { var updateTimestamp: Int64 = 1 var splitsFilterQueryString: String = "" var flagsSpec: String = "" + internal var segmentsInUse: Int64 = 0 private let inMemorySplits = ConcurrentDictionary() @@ -75,4 +76,8 @@ class LocalhostSplitsStorage: SplitsStorage { func destroy() { inMemorySplits.removeAll() } + + func forceParsing() { + + } } diff --git a/Split/Network/Sync/FeatureFlagsSynchronizer.swift b/Split/Network/Sync/FeatureFlagsSynchronizer.swift index 0183d622e..9c73d4d02 100644 --- a/Split/Network/Sync/FeatureFlagsSynchronizer.swift +++ b/Split/Network/Sync/FeatureFlagsSynchronizer.swift @@ -78,13 +78,26 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { return } - let splitsStorage = self.storageContainer.splitsStorage + let splitsStorage = storageContainer.splitsStorage let ruleBasedSegmentsStorage = storageContainer.ruleBasedSegmentsStorage - DispatchQueue.general.async { + + DispatchQueue.general.async { [weak self] in + guard let self = self else { return } + let start = Date.nowMillis() self.filterSplitsInCache() + + // Part of /memberships hits optimization + if self.storageContainer.generalInfoStorage.getSegmentsInUse() == nil { + splitsStorage.forceParsing() + ruleBasedSegmentsStorage.forceParsing() + } + + // Load local splitsStorage.loadLocal() ruleBasedSegmentsStorage.loadLocal() + + // Events & Logs if splitsStorage.getAll().count > 0 { self.splitEventsManager.notifyInternalEvent(.splitsLoadedFromCache) } diff --git a/Split/Network/Sync/MySegmentsSynchronizer.swift b/Split/Network/Sync/MySegmentsSynchronizer.swift index 2638cf48f..efdc2bcfc 100644 --- a/Split/Network/Sync/MySegmentsSynchronizer.swift +++ b/Split/Network/Sync/MySegmentsSynchronizer.swift @@ -19,6 +19,7 @@ protocol MySegmentsSynchronizer { func destroy() } +// One instance per client class DefaultMySegmentsSynchronizer: MySegmentsSynchronizer { private let mySegmentsStorage: ByKeyMySegmentsStorage @@ -98,6 +99,7 @@ class DefaultMySegmentsSynchronizer: MySegmentsSynchronizer { mySegmentsSyncWorker.start() } + // Used for streaming func forceMySegmentsSync(changeNumbers: SegmentsChangeNumber, delay: Int64) { if isDestroyed.value { return @@ -149,12 +151,6 @@ class DefaultMySegmentsSynchronizer: MySegmentsSynchronizer { return } - syncChangeNumbers?.mutate { - if $0.msChangeNumber <= changeNumbers.msChangeNumber, - changeNumbers.mlsChangeNumber <= changeNumbers.mlsChangeNumber { - } - } - if timerManager?.isScheduled(timer: .syncSegments) ?? false { return } diff --git a/Split/Storage/GeneralInfo/GeneralInfoDao.swift b/Split/Storage/GeneralInfo/GeneralInfoDao.swift index ad27610a6..6456e07b2 100644 --- a/Split/Storage/GeneralInfo/GeneralInfoDao.swift +++ b/Split/Storage/GeneralInfo/GeneralInfoDao.swift @@ -18,6 +18,7 @@ enum GeneralInfo: String { case rolloutCacheLastClearTimestamp = "rolloutCacheLastClearTimestamp" case ruleBasedSegmentsChangeNumber = "ruleBasedSegmentsChangeNumber" case lastProxyUpdateTimestamp = "lastProxyCheckTimestamp" + case segmentsInUse = "segmentsInUse" } protocol GeneralInfoDao { diff --git a/Split/Storage/GeneralInfo/GeneralInfoStorage.swift b/Split/Storage/GeneralInfo/GeneralInfoStorage.swift index a728f7d21..6193c4e0b 100644 --- a/Split/Storage/GeneralInfo/GeneralInfoStorage.swift +++ b/Split/Storage/GeneralInfo/GeneralInfoStorage.swift @@ -17,6 +17,10 @@ protocol GeneralInfoStorage { // Proxy handling methods func getLastProxyUpdateTimestamp() -> Int64 func setLastProxyUpdateTimestamp(_ timestamp: Int64) + + // Segments in use (for /memberships optimization) + func getSegmentsInUse() -> Int64? + func setSegmentsInUse(_ count: Int64) } class DefaultGeneralInfoStorage: GeneralInfoStorage { @@ -78,4 +82,12 @@ class DefaultGeneralInfoStorage: GeneralInfoStorage { func setLastProxyUpdateTimestamp(_ timestamp: Int64) { generalInfoDao.update(info: .lastProxyUpdateTimestamp, longValue: timestamp) } + + func getSegmentsInUse() -> Int64? { + generalInfoDao.longValue(info: .segmentsInUse) + } + + func setSegmentsInUse(_ count: Int64) { + generalInfoDao.update(info: .segmentsInUse, longValue: count) + } } diff --git a/Split/Storage/MySegments/ByKeyMySegmentsStorage.swift b/Split/Storage/MySegments/ByKeyMySegmentsStorage.swift index f8228c07c..af50dbcee 100644 --- a/Split/Storage/MySegments/ByKeyMySegmentsStorage.swift +++ b/Split/Storage/MySegments/ByKeyMySegmentsStorage.swift @@ -8,7 +8,7 @@ import Foundation -protocol ByKeyMySegmentsStorage { +protocol ByKeyMySegmentsStorage: SegmentsChecker { var changeNumber: Int64 { get } func loadLocal() func getAll() -> Set @@ -16,6 +16,7 @@ protocol ByKeyMySegmentsStorage { func getCount() -> Int } +// One instance per client class DefaultByKeyMySegmentsStorage: ByKeyMySegmentsStorage { private let mySegmentsStorage: MySegmentsStorage @@ -48,4 +49,9 @@ class DefaultByKeyMySegmentsStorage: ByKeyMySegmentsStorage { func getCount() -> Int { return mySegmentsStorage.getCount(forKey: userKey) } + + // MARK: For Network Traffic Optimization + func isUsingSegments() -> Bool { + mySegmentsStorage.isUsingSegments() + } } diff --git a/Split/Storage/MySegments/MyLargeSegmentsStorage.swift b/Split/Storage/MySegments/MyLargeSegmentsStorage.swift index ac9d63ecb..0a815f3d2 100644 --- a/Split/Storage/MySegments/MyLargeSegmentsStorage.swift +++ b/Split/Storage/MySegments/MyLargeSegmentsStorage.swift @@ -15,13 +15,15 @@ class MyLargeSegmentsStorage: MySegmentsStorage { private let defaultChangeNumber = ServiceConstants.defaultSegmentsChangeNumber private let syncQueue: DispatchQueue private let syncQueueKey = DispatchSpecificKey() + private let generalInfoStorage: GeneralInfoStorage var keys: Set { return inMemorySegments.keys } - init(persistentStorage: PersistentMySegmentsStorage) { + init(persistentStorage: PersistentMySegmentsStorage, generalInfoStorage: GeneralInfoStorage) { self.persistentStorage = persistentStorage + self.generalInfoStorage = generalInfoStorage self.syncQueue = DispatchQueue(label: "split-large-segments-storage") syncQueue.setSpecific(key: syncQueueKey, value: ()) } @@ -92,4 +94,9 @@ class MyLargeSegmentsStorage: MySegmentsStorage { return syncQueue.sync(execute: block) } } + + // MARK: For Network Traffic Optimization + func isUsingSegments() -> Bool { + (generalInfoStorage.getSegmentsInUse() ?? 0) > 0 + } } diff --git a/Split/Storage/MySegments/MySegmentsStorage.swift b/Split/Storage/MySegments/MySegmentsStorage.swift index 2cd0bb837..88fcb5240 100644 --- a/Split/Storage/MySegments/MySegmentsStorage.swift +++ b/Split/Storage/MySegments/MySegmentsStorage.swift @@ -8,7 +8,7 @@ import Foundation -protocol MySegmentsStorage: RolloutDefinitionsCache { +protocol MySegmentsStorage: RolloutDefinitionsCache, SegmentsChecker { var keys: Set { get } func loadLocal(forKey key: String) func changeNumber(forKey key: String) -> Int64? @@ -21,17 +21,24 @@ protocol MySegmentsStorage: RolloutDefinitionsCache { func getCount() -> Int } +protocol SegmentsChecker { + func isUsingSegments() -> Bool +} + +// One instance per factory class DefaultMySegmentsStorage: MySegmentsStorage { private var inMemoryMySegments: SynchronizedDictionarySet = SynchronizedDictionarySet() private let persistenStorage: PersistentMySegmentsStorage + private let generalInfoStorage: GeneralInfoStorage var keys: Set { return inMemoryMySegments.keys } - init(persistentMySegmentsStorage: PersistentMySegmentsStorage) { + init(persistentMySegmentsStorage: PersistentMySegmentsStorage, generalInfoStorage: GeneralInfoStorage) { persistenStorage = persistentMySegmentsStorage + self.generalInfoStorage = generalInfoStorage } func loadLocal(forKey key: String) { @@ -83,4 +90,9 @@ class DefaultMySegmentsStorage: MySegmentsStorage { inMemoryMySegments.removeAll() persistenStorage.deleteAll() } + + // MARK: For Network Traffic Optimization + func isUsingSegments() -> Bool { + (generalInfoStorage.getSegmentsInUse() ?? 0) > 0 + } } diff --git a/Split/Storage/RuleBasedSegments/PersistentRuleBasedSegmentsStorage.swift b/Split/Storage/RuleBasedSegments/PersistentRuleBasedSegmentsStorage.swift index 5facb7253..42de8af24 100644 --- a/Split/Storage/RuleBasedSegments/PersistentRuleBasedSegmentsStorage.swift +++ b/Split/Storage/RuleBasedSegments/PersistentRuleBasedSegmentsStorage.swift @@ -13,6 +13,9 @@ protocol PersistentRuleBasedSegmentsStorage { func update(toAdd: Set, toRemove: Set, changeNumber: Int64) func clear() func getChangeNumber() -> Int64 + + func getSegmentsInUse() -> Int64? + func setSegmentsInUse(_ segmentsInUse: Int64) } class DefaultPersistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStorage { @@ -55,4 +58,12 @@ class DefaultPersistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStor func getChangeNumber() -> Int64 { return generalInfoStorage.getRuleBasedSegmentsChangeNumber() } + + func getSegmentsInUse() -> Int64? { + generalInfoStorage.getSegmentsInUse() + } + + func setSegmentsInUse(_ segmentsInUse: Int64) { + generalInfoStorage.setSegmentsInUse(segmentsInUse) + } } diff --git a/Split/Storage/RuleBasedSegments/RuleBasedSegmentsStorage.swift b/Split/Storage/RuleBasedSegments/RuleBasedSegmentsStorage.swift index 7d5efcb5a..a9a11b165 100644 --- a/Split/Storage/RuleBasedSegments/RuleBasedSegmentsStorage.swift +++ b/Split/Storage/RuleBasedSegments/RuleBasedSegmentsStorage.swift @@ -10,11 +10,13 @@ import Foundation protocol RuleBasedSegmentsStorage: RolloutDefinitionsCache { var changeNumber: Int64 { get } + var segmentsInUse: Int64 { get } func get(segmentName: String) -> RuleBasedSegment? func contains(segmentNames: Set) -> Bool func update(toAdd: Set, toRemove: Set, changeNumber: Int64) -> Bool func loadLocal() + func forceParsing() // For Lazy Parsing optimization } class DefaultRuleBasedSegmentsStorage: RuleBasedSegmentsStorage { @@ -23,6 +25,8 @@ class DefaultRuleBasedSegmentsStorage: RuleBasedSegmentsStorage { private var inMemorySegments: ConcurrentDictionary private(set) var changeNumber: Int64 = -1 + + internal var segmentsInUse: Int64 = 0 init(persistentStorage: PersistentRuleBasedSegmentsStorage) { self.persistentStorage = persistentStorage @@ -30,39 +34,29 @@ class DefaultRuleBasedSegmentsStorage: RuleBasedSegmentsStorage { } func loadLocal() { + segmentsInUse = persistentStorage.getSegmentsInUse() ?? 0 let snapshot = persistentStorage.getSnapshot() let active = snapshot.segments.filter { $0.status == .active } let archived = snapshot.segments.filter { $0.status == .archived } - // Process active segments - for segment in active { - if let segmentName = segment.name?.lowercased() { - inMemorySegments.setValue(segment, forKey: segmentName) - } - } - - // Process archived segments - remove them from memory if they exist - for segment in archived { - if let segmentName = segment.name?.lowercased() { - inMemorySegments.removeValue(forKey: segmentName) - } - } + _ = processToAdd(Set(active)) + _ = processToRemove(Set(archived)) changeNumber = snapshot.changeNumber + persistentStorage.setSegmentsInUse(segmentsInUse) } func get(segmentName: String) -> RuleBasedSegment? { - guard let segment = inMemorySegments.value(forKey: segmentName.lowercased()) else { - return nil - } + guard let segment = inMemorySegments.value(forKey: segmentName.lowercased()) else { return nil } - if !segment.isParsed { - if let parsed = try? Json.decodeFrom(json: segment.json, to: RuleBasedSegment.self) { - inMemorySegments.setValue(parsed, forKey: segmentName.lowercased()) - return parsed + if !segment.isParsed { // Parse if neccesaty (Lazy Parsing) + if let parsedSegment = parseSegment(segment) { + inMemorySegments.setValue(parsedSegment, forKey: segmentName.lowercased()) + return parsedSegment } return nil } + return segment } @@ -73,30 +67,45 @@ class DefaultRuleBasedSegmentsStorage: RuleBasedSegmentsStorage { } func update(toAdd: Set, toRemove: Set, changeNumber: Int64) -> Bool { - var updated = false + + segmentsInUse = persistentStorage.getSegmentsInUse() ?? 0 + self.changeNumber = changeNumber + + // Process + let addResult = processToAdd(toAdd) + let removeResult = processToRemove(toRemove) + + // Update persistent storage + persistentStorage.update(toAdd: toAdd, toRemove: toRemove, changeNumber: changeNumber) + persistentStorage.setSegmentsInUse(segmentsInUse) + + return addResult || removeResult + } - // Process segments to add + private func processToAdd(_ toAdd: Set) -> Bool { // Process segments to add + var result = false + for segment in toAdd { if let segmentName = segment.name?.lowercased() { + updateSegmentsCount(segment) inMemorySegments.setValue(segment, forKey: segmentName) - updated = true + result = true } } - - // Process segments to remove + return result + } + + private func processToRemove(_ toRemove: Set) -> Bool { // Process segments to remove + var result = false + for segment in toRemove { if let segmentName = segment.name?.lowercased(), inMemorySegments.value(forKey: segmentName) != nil { + updateSegmentsCount(segment) inMemorySegments.removeValue(forKey: segmentName) - updated = true + result = true } } - - self.changeNumber = changeNumber - - // Update persistent storage - persistentStorage.update(toAdd: toAdd, toRemove: toRemove, changeNumber: changeNumber) - - return updated + return result } func clear() { @@ -104,4 +113,39 @@ class DefaultRuleBasedSegmentsStorage: RuleBasedSegmentsStorage { changeNumber = -1 persistentStorage.clear() } + + func forceParsing() { + segmentsInUse = persistentStorage.getSegmentsInUse() ?? 0 + let activeSegments = persistentStorage.getSnapshot().segments.filter { $0.status == .active } + + for i in 0.. RuleBasedSegment? { + guard let parsedSegment = try? Json.decodeFrom(json: segment.json, to: RuleBasedSegment.self) else { return nil } + return parsedSegment + } + + fileprivate func updateSegmentsCount(_ segment: RuleBasedSegment) { + if let segmentName = segment.name?.lowercased(), segment.status == .active, inMemorySegments.value(forKey: segmentName) == nil, StorageHelper.usesSegments(segment.conditions) { + segmentsInUse += 1 + } else if inMemorySegments.value(forKey: segment.name?.lowercased() ?? "") != nil, segment.status != .active, StorageHelper.usesSegments(segment.conditions) { + segmentsInUse -= 1 + } + } + + #if DEBUG + func getInMemorySegments() -> ConcurrentDictionary { + inMemorySegments + } + #endif } diff --git a/Split/Storage/Splits/PersistentSplitsStorage.swift b/Split/Storage/Splits/PersistentSplitsStorage.swift index b082b5825..e6faf2567 100644 --- a/Split/Storage/Splits/PersistentSplitsStorage.swift +++ b/Split/Storage/Splits/PersistentSplitsStorage.swift @@ -12,10 +12,12 @@ protocol PersistentSplitsStorage { func update(splitChange: ProcessedSplitChange) func update(split: Split) func update(bySetsFilter: SplitFilter?) + func update(segmentsInUse: Int64) func getBySetsFilter() -> SplitFilter? func getSplitsSnapshot() -> SplitsSnapshot func getChangeNumber() -> Int64 func getUpdateTimestamp() -> Int64 + func getSegmentsInUse() -> Int64? func getAll() -> [Split] func delete(splitNames: [String]) func clear() @@ -49,6 +51,10 @@ class DefaultPersistentSplitsStorage: PersistentSplitsStorage { func update(flagsSpec: String) { generalInfoDao.update(info: .flagsSpec, stringValue: flagsSpec) } + + func update(segmentsInUse: Int64) { + generalInfoDao.update(info: .segmentsInUse, longValue: segmentsInUse) + } func getFilterQueryString() -> String { return generalInfoDao.stringValue(info: .splitsFilterQueryString) ?? "" @@ -57,6 +63,10 @@ class DefaultPersistentSplitsStorage: PersistentSplitsStorage { func getFlagsSpec() -> String { return generalInfoDao.stringValue(info: .flagsSpec) ?? "" } + + func getSegmentsInUse() -> Int64? { + generalInfoDao.longValue(info: .segmentsInUse) + } func update(bySetsFilter filter: SplitFilter?) { guard let filter = filter else { diff --git a/Split/Storage/Splits/SplitsStorage.swift b/Split/Storage/Splits/SplitsStorage.swift index 0b7008308..6484862ba 100644 --- a/Split/Storage/Splits/SplitsStorage.swift +++ b/Split/Storage/Splits/SplitsStorage.swift @@ -15,6 +15,7 @@ protocol SyncSplitsStorage: RolloutDefinitionsCache { protocol SplitsStorage: SyncSplitsStorage { var changeNumber: Int64 { get } var updateTimestamp: Int64 { get } + var segmentsInUse: Int64 { get } func loadLocal() func get(name: String) -> Split? @@ -26,15 +27,17 @@ protocol SplitsStorage: SyncSplitsStorage { func isValidTrafficType(name: String) -> Bool func getCount() -> Int func destroy() + func forceParsing() } class DefaultSplitsStorage: SplitsStorage { private var persistentStorage: PersistentSplitsStorage - private var inMemorySplits: ConcurrentDictionary private var trafficTypes: ConcurrentDictionary private let flagSetsCache: FlagSetsCache - + private var inMemorySplits: ConcurrentDictionary + internal var segmentsInUse: Int64 = 0 + private(set) var changeNumber: Int64 = -1 private(set) var updateTimestamp: Int64 = -1 @@ -47,6 +50,7 @@ class DefaultSplitsStorage: SplitsStorage { } func loadLocal() { + segmentsInUse = persistentStorage.getSegmentsInUse() ?? 0 let snapshot = persistentStorage.getSplitsSnapshot() let active = snapshot.splits.filter { $0.status == .active } let archived = snapshot.splits.filter { $0.status == .archived } @@ -58,24 +62,13 @@ class DefaultSplitsStorage: SplitsStorage { func get(name: String) -> Split? { let lowercasedName = name.lowercased() + guard let split = inMemorySplits.value(forKey: lowercasedName) else { return nil } - guard let split = inMemorySplits.value(forKey: lowercasedName) else { - return nil - } - if !split.isCompletelyParsed { - if let parsed = try? Json.decodeFrom(json: split.json, to: Split.self) { - if isUnsupportedMatcher(split: parsed) { - parsed.conditions = [SplitHelper.createDefaultCondition()] - } - - parsed.isCompletelyParsed = true - inMemorySplits.setValue(parsed, forKey: lowercasedName) - return parsed - } - return nil - } else if isUnsupportedMatcher(split: split) { - split.conditions = [SplitHelper.createDefaultCondition()] + if split.isCompletelyParsed != true { // Parse if necessary (lazy parsing) + let parsedSplit = parseSplit(split) + inMemorySplits.setValue(parsedSplit, forKey: lowercasedName) } + return split } @@ -89,12 +82,16 @@ class DefaultSplitsStorage: SplitsStorage { } func update(splitChange: ProcessedSplitChange) -> Bool { + + // Process let updated = processUpdated(splits: splitChange.activeSplits, active: true) let removed = processUpdated(splits: splitChange.archivedSplits, active: false) + // Update changeNumber = splitChange.changeNumber updateTimestamp = splitChange.updateTimestamp persistentStorage.update(splitChange: splitChange) + return updated || removed } @@ -130,6 +127,7 @@ class DefaultSplitsStorage: SplitsStorage { var splitsRemoved = false for split in splits { + guard let splitName = split.name?.lowercased() else { Logger.e("Invalid feature flag name received while updating feature flags") continue @@ -146,9 +144,12 @@ class DefaultSplitsStorage: SplitsStorage { // Split to remove not in memory, do nothing continue } + + // Smart Pausing optimization + updateSegmentsCount(split: split) if loadedSplit != nil, let oldTrafficType = loadedSplit?.trafficTypeName { - // Must decreated old traffic type count if a feature flag is updated or removed + // Must decrease old traffic type count if a feature flag is updated or removed let count = cachedTrafficTypes[oldTrafficType] ?? 0 if count > 1 { cachedTrafficTypes[oldTrafficType] = count - 1 @@ -172,12 +173,28 @@ class DefaultSplitsStorage: SplitsStorage { } inMemorySplits.setValues(cachedSplits) trafficTypes.setValues(cachedTrafficTypes) + persistentStorage.update(segmentsInUse: segmentsInUse) // Ensure count of Flags with Segments (for optimization feature) return splitsUpdated || splitsRemoved } func destroy() { inMemorySplits.removeAll() } + + @discardableResult private func parseSplit(_ split: Split) -> Split { // Lazy parsing feature + if !split.isCompletelyParsed { + if let parsed = try? Json.decodeFrom(json: split.json, to: Split.self) { + if isUnsupportedMatcher(split: parsed) { + parsed.conditions = [SplitHelper.createDefaultCondition()] + } + parsed.isCompletelyParsed = true + return parsed + } + } else if isUnsupportedMatcher(split: split) { + split.conditions = [SplitHelper.createDefaultCondition()] + } + return split + } private func isUnsupportedMatcher(split: Split?) -> Bool { var result = false @@ -205,6 +222,40 @@ class DefaultSplitsStorage: SplitsStorage { return result } + + func forceParsing() { // Parse all Splits + segmentsInUse = 0 + let activeSplits = persistentStorage.getSplitsSnapshot().splits.filter( { $0.status == .active } ) + + if activeSplits.count > 0 { + for i in 0...activeSplits.count-1 { + guard let splitName = activeSplits[i].name else { continue } + let parsedSplit = parseSplit(activeSplits[i]) + updateSegmentsCount(split: parsedSplit) + inMemorySplits.setValue(parsedSplit, forKey: splitName) + } + } + + persistentStorage.update(segmentsInUse: segmentsInUse) + } + + func updateSegmentsCount(split: Split) { // Keep count of Flags with Segments (used to optimize "/memberships" hits) + guard let splitName = split.name else { return } + + if inMemorySplits.value(forKey: splitName) == nil, StorageHelper.usesSegments(split.conditions ?? []) { + if split.status == .active { // If new Split and active + segmentsInUse += 1 + } else if inMemorySplits.value(forKey: splitName) != nil && split.status != .active { // If known Split and archived + segmentsInUse -= 1 + } + } + } + + #if DEBUG + func getInMemorySplits() -> ConcurrentDictionary { + inMemorySplits + } + #endif } class BackgroundSyncSplitsStorage: SyncSplitsStorage { diff --git a/Split/Storage/StorageHelper.swift b/Split/Storage/StorageHelper.swift new file mode 100644 index 000000000..a5df7420b --- /dev/null +++ b/Split/Storage/StorageHelper.swift @@ -0,0 +1,19 @@ +// Created by Martin Cardozo on 15/07/2025 + +// ⚠️ Don't change to struct. This enum without cases is used as a namespace, and forbids accidental instantiation +enum StorageHelper { + + static func usesSegments(_ conditions: [Condition]?) -> Bool { + guard let conditions = conditions else { return false } + + for condition in conditions { + let matchers = condition.matcherGroup?.matchers ?? [] + for matcher in matchers { + if matcher.matcherType == .inSegment || matcher.matcherType == .inLargeSegment { + return true + } + } + } + return false + } +} diff --git a/Split/Storage/split_cache.xcdatamodeld/split_cache_v6.xcdatamodel/contents b/Split/Storage/split_cache.xcdatamodeld/split_cache_v6.xcdatamodel/contents index 15cbe9b35..8e8362481 100644 --- a/Split/Storage/split_cache.xcdatamodeld/split_cache_v6.xcdatamodel/contents +++ b/Split/Storage/split_cache.xcdatamodeld/split_cache_v6.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/SplitTests/Fake/ByKeyMySegmentsStorageStub.swift b/SplitTests/Fake/ByKeyMySegmentsStorageStub.swift index 751c5cf0e..7cc2728df 100644 --- a/SplitTests/Fake/ByKeyMySegmentsStorageStub.swift +++ b/SplitTests/Fake/ByKeyMySegmentsStorageStub.swift @@ -16,6 +16,7 @@ class ByKeyMySegmentsStorageStub: ByKeyMySegmentsStorage { var updatedSegments: [String]? var loadLocalCalled = false var clearCalled = false + var isUsingSegmentsCalled = false var updateExpectation: XCTestExpectation? var clearExpectation: XCTestExpectation? var getCountCalledCount = 0 @@ -52,4 +53,8 @@ class ByKeyMySegmentsStorageStub: ByKeyMySegmentsStorage { getCountCalledCount+=1 return segments.count } + + func isUsingSegments() -> Bool { + segments.count != 0 + } } diff --git a/SplitTests/Fake/Storage/GeneralInfoStorageMock.swift b/SplitTests/Fake/Storage/GeneralInfoStorageMock.swift index 75da1761a..de21a1670 100644 --- a/SplitTests/Fake/Storage/GeneralInfoStorageMock.swift +++ b/SplitTests/Fake/Storage/GeneralInfoStorageMock.swift @@ -2,6 +2,7 @@ import Foundation @testable import Split class GeneralInfoStorageMock: GeneralInfoStorage { + let queue = DispatchQueue(label: "test", target: .global()) var updateTimestamp: Int64 = 0 var rolloutCacheLastClearTimestamp: Int64 = 0 @@ -9,6 +10,7 @@ class GeneralInfoStorageMock: GeneralInfoStorage { var flagsSpec = "" var ruleBasedSegmentsChangeNumber: Int64 = -1 var lastProxyUpdateTimestamp: Int64 = 0 + var segmentsInUse: Int64 = 0 func getUpdateTimestamp() -> Int64 { return updateTimestamp @@ -57,4 +59,12 @@ class GeneralInfoStorageMock: GeneralInfoStorage { func setLastProxyUpdateTimestamp(_ timestamp: Int64) { lastProxyUpdateTimestamp = timestamp } + + func getSegmentsInUse() -> Int64? { + segmentsInUse + } + + func setSegmentsInUse(_ count: Int64) { + segmentsInUse = count + } } diff --git a/SplitTests/Fake/Storage/MySegmentsStorageStub.swift b/SplitTests/Fake/Storage/MySegmentsStorageStub.swift index 977eed9a6..072d466ba 100644 --- a/SplitTests/Fake/Storage/MySegmentsStorageStub.swift +++ b/SplitTests/Fake/Storage/MySegmentsStorageStub.swift @@ -25,7 +25,6 @@ class MySegmentsStorageStub: MySegmentsStorage { return Set(segments.keys.map { $0 }) } - func changeNumber(forKey key: String) -> Int64? { return changeNumber } @@ -88,4 +87,11 @@ class MySegmentsStorageStub: MySegmentsStorage { clearCalledTimes+=1 segments.removeAll() } + + var segmentsInUse = 0 + var isUsingSegmentsCalled = false + func isUsingSegments() -> Bool { + isUsingSegmentsCalled = true + return segmentsInUse > 0 + } } diff --git a/SplitTests/Fake/Storage/PersistentRuleBasedSegmentsStorageStub.swift b/SplitTests/Fake/Storage/PersistentRuleBasedSegmentsStorageStub.swift index 0ffeee907..2c8fb9138 100644 --- a/SplitTests/Fake/Storage/PersistentRuleBasedSegmentsStorageStub.swift +++ b/SplitTests/Fake/Storage/PersistentRuleBasedSegmentsStorageStub.swift @@ -59,4 +59,13 @@ class PersistentRuleBasedSegmentsStorageStub: PersistentRuleBasedSegmentsStorage changeNumberCalled = true return delegate?.getChangeNumber() ?? -1 } + + var segmentsInUse: Int64 = 0 + func getSegmentsInUse() -> Int64? { + segmentsInUse + } + + func setSegmentsInUse(_ segmentsInUse: Int64) { + self.segmentsInUse = segmentsInUse + } } diff --git a/SplitTests/Fake/Storage/PersistentSplitsStorageStub.swift b/SplitTests/Fake/Storage/PersistentSplitsStorageStub.swift index 1af6a31cc..17454878c 100644 --- a/SplitTests/Fake/Storage/PersistentSplitsStorageStub.swift +++ b/SplitTests/Fake/Storage/PersistentSplitsStorageStub.swift @@ -21,6 +21,7 @@ class PersistentSplitsStorageStub: PersistentSplitsStorage { var getAllCalled = false var updateCalled = false + var getSegmentsInUseCalled = false var deleteCalled = false var clearCalled = false var closeCalled = false @@ -32,6 +33,16 @@ class PersistentSplitsStorageStub: PersistentSplitsStorage { var updateFlagsSpecCalled = false var splits = [String: Split]() var lastBySetSplitFilter: SplitFilter? + + private let delegate: PersistentSplitsStorage? + + init(delegate: PersistentSplitsStorage?) { + self.delegate = delegate + } + + convenience init() { + self.init(delegate: nil) + } func update(splitChange: ProcessedSplitChange) { processedSplitChange = splitChange @@ -46,7 +57,7 @@ class PersistentSplitsStorageStub: PersistentSplitsStorage { } func getSplitsSnapshot() -> SplitsSnapshot { - return snapshot + return delegate?.getSplitsSnapshot() ?? snapshot } func getAll() -> [Split] { @@ -61,6 +72,7 @@ class PersistentSplitsStorageStub: PersistentSplitsStorage { func clear() { clearCalled = true + delegate?.clear() } func close() { @@ -86,4 +98,15 @@ class PersistentSplitsStorageStub: PersistentSplitsStorage { getBySetsFilterCalled = false return nil } + + var segmentsInUse: Int64 = 0 + func update(segmentsInUse: Int64) { + self.segmentsInUse = segmentsInUse + delegate?.update(segmentsInUse: segmentsInUse) + } + + func getSegmentsInUse() -> Int64? { + getSegmentsInUseCalled = true + return segmentsInUse + } } diff --git a/SplitTests/Fake/Storage/RuleBasedSegmentsStorageStub.swift b/SplitTests/Fake/Storage/RuleBasedSegmentsStorageStub.swift index 6d8dfeb12..eb36c6b67 100644 --- a/SplitTests/Fake/Storage/RuleBasedSegmentsStorageStub.swift +++ b/SplitTests/Fake/Storage/RuleBasedSegmentsStorageStub.swift @@ -13,6 +13,8 @@ class RuleBasedSegmentsStorageStub: RuleBasedSegmentsStorage { var segments = [String: RuleBasedSegment]() var changeNumber: Int64 = -1 + + var segmentsInUse: Int64 = 0 var getCalled = false var containsCalled = false @@ -74,4 +76,9 @@ class RuleBasedSegmentsStorageStub: RuleBasedSegmentsStorage { func loadLocal() { loadLocalCalled = true } + + var forcedReparse: Bool = false + func forceParsing() { + forcedReparse = true + } } diff --git a/SplitTests/Fake/Storage/SplitsStorageStub.swift b/SplitTests/Fake/Storage/SplitsStorageStub.swift index 65e5a4608..5fd1f82e9 100644 --- a/SplitTests/Fake/Storage/SplitsStorageStub.swift +++ b/SplitTests/Fake/Storage/SplitsStorageStub.swift @@ -21,6 +21,8 @@ class SplitsStorageStub: SplitsStorage { var splitsFilterQueryString: String = "" var flagsSpec: String = "" + + var segmentsInUse: Int64 = 0 var loadLocalCalled = false var clearCalledTimes = 0 @@ -114,4 +116,9 @@ class SplitsStorageStub: SplitsStorage { func update(bySetsFilter: SplitFilter?) { updateBySetsFilterCount+=1 } + + var forceReparsingCalled = false + func forceParsing() { + forceReparsingCalled = true + } } diff --git a/SplitTests/Helpers/SplitTestHelper.swift b/SplitTests/Helpers/SplitTestHelper.swift index c797a14bb..e47bc8039 100644 --- a/SplitTests/Helpers/SplitTestHelper.swift +++ b/SplitTests/Helpers/SplitTestHelper.swift @@ -177,7 +177,6 @@ class SplitTestHelper { } static func newSplit(name: String, trafficType: String) -> Split { - let splitJSONExample = """ { "trafficTypeName":"\(trafficType)", @@ -300,4 +299,14 @@ class SplitTestHelper { split.isCompletelyParsed = false return split } + + static func newSplitWithMatcherType(_ name: String, _ matcher: MatcherType, parsed: Bool = true) -> Split { + let split = Split(name: name, trafficType: "User", status: .active, sets: nil, json: "") + split.isCompletelyParsed = parsed + split.conditions = [Condition()] + split.conditions?[0].matcherGroup = MatcherGroup() + split.conditions?[0].matcherGroup!.matchers = [Matcher()] + split.conditions?[0].matcherGroup!.matchers![0].matcherType = matcher + return split + } } diff --git a/SplitTests/Helpers/TestSplitFactory.swift b/SplitTests/Helpers/TestSplitFactory.swift index 56a872800..2f4c373f7 100644 --- a/SplitTests/Helpers/TestSplitFactory.swift +++ b/SplitTests/Helpers/TestSplitFactory.swift @@ -67,7 +67,7 @@ class TestSplitFactory: SplitFactory { self.httpClient = DefaultHttpClient(session: session, requestManager: reqManager) } - func buildSdk() throws { + func buildSdk(polling: Bool = false) throws { guard let httpClient = self.httpClient else { print("HTTP client is null. Fix!!") @@ -200,6 +200,8 @@ class TestSplitFactory: SplitFactory { rolloutCacheConfiguration: splitConfig.rolloutCacheConfiguration ?? RolloutCacheConfiguration.builder().build(), storages: storageContainer.splitsStorage, storageContainer.mySegmentsStorage, storageContainer.myLargeSegmentsStorage) + if polling { splitConfig.streamingEnabled = false } + clientManager = DefaultClientManager(config: splitConfig, key: key, splitManager: manager, diff --git a/SplitTests/Integration/streaming/MySegmentUpdateTest.swift b/SplitTests/Integration/streaming/MySegmentUpdateTest.swift index 90d12b5fb..34eddb1f6 100644 --- a/SplitTests/Integration/streaming/MySegmentUpdateTest.swift +++ b/SplitTests/Integration/streaming/MySegmentUpdateTest.swift @@ -21,27 +21,27 @@ class MySegmentUpdateTest: XCTestCase { var notificationTemplate: String! let kDataField = "[NOTIFICATION_DATA]" var msHit = 0 - + let kRefreshRate = 1 - + var mySegExp: XCTestExpectation! - + var testFactory: TestSplitFactory! var queue = DispatchQueue(label: "pepe") - + override func setUp() { hitCountByKey = [String: Int]() loadNotificationTemplate() } - + func testMyLargeSegmentsUpdate() throws { try mySegmentsUpdateTest(type: .myLargeSegmentsUpdate) } - + func testMySegmentsUpdate() throws { try mySegmentsUpdateTest(type: .mySegmentsUpdate) } - + func mySegmentsUpdateTest(type: NotificationType) throws { let userKey = "key1" testFactory = TestSplitFactory(userKey: userKey) @@ -51,25 +51,25 @@ class MySegmentUpdateTest: XCTestCase { let syncSpy = testFactory.synchronizerSpy let client = testFactory.client let db = testFactory.splitDatabase - + let sdkReadyExp = XCTestExpectation(description: "SDK READY Expectation") var sdkUpdExp = XCTestExpectation(description: "SDK UPDATE Expectation") - + client.on(event: SplitEvent.sdkReady) { sdkReadyExp.fulfill() } - + client.on(event: SplitEvent.sdkUpdated) { sdkUpdExp.fulfill() } - + // Wait for hitting my segments two times (sdk ready and full sync after streaming connection) wait(for: [sdkReadyExp, sseExp], timeout: 50) - + streamingBinding?.push(message: ":keepalive") - + wait(for: [mySegExp], timeout: 5) - + // Unbounded fetch notification should trigger my segments // refresh on synchronizer // Set count to 0 to start counting hits @@ -77,25 +77,25 @@ class MySegmentUpdateTest: XCTestCase { sdkUpdExp = XCTestExpectation() pushMessage(TestingData.unboundedNotification(type: type, cn: mySegmentsCns[cnIndex()])) wait(for: [sdkUpdExp], timeout: 5) - + // Should not trigger any fetch to my segments because // this payload doesn't have "key1" enabled - + Thread.sleep(forTimeInterval: 0.5) pushMessage(TestingData.escapedBoundedNotificationZlib(type: type, cn: mySegmentsCns[cnIndex()])) - + // Pushed key list message. Key 1 should add a segment sdkUpdExp = XCTestExpectation() - + Thread.sleep(forTimeInterval: 0.5) pushMessage(TestingData.escapedKeyListNotificationGzip(type: type, cn: mySegmentsCns[cnIndex()])) wait(for: [sdkUpdExp], timeout: 5) - + sdkUpdExp = XCTestExpectation() Thread.sleep(forTimeInterval: 0.5) pushMessage(TestingData.segmentRemovalNotification(type: type, cn: mySegmentsCns[cnIndex()])) wait(for: [sdkUpdExp], timeout: 5) - + Thread.sleep(forTimeInterval: 2.0) var segmentEntity: [String]! if type == .mySegmentsUpdate { @@ -103,18 +103,453 @@ class MySegmentUpdateTest: XCTestCase { } else { segmentEntity = db.myLargeSegmentsDao.getBy(userKey: testFactory.userKey)?.segments.map { $0.name } ?? [] } - + // Hits are not asserted because tests will fail if expectations are not fulfilled XCTAssertEqual(1, syncSpy.forceMySegmentsSyncCount[userKey] ?? 0) XCTAssertEqual(1, segmentEntity.filter { $0 == "new_segment_added" }.count) XCTAssertEqual(0, segmentEntity.filter { $0 == "segment1" }.count) - + let semaphore = DispatchSemaphore(value: 0) client.destroy(completion: { _ = semaphore.signal() }) semaphore.wait() } + + func testSdkReadyWaitsForSegments() throws { + + var sdkReadyFired = false + let userKey = "test-user-key" + + let sdkReady = XCTestExpectation(description: "SDK should be ready") + let segmentsHit = XCTestExpectation(description: "/memberships should be hit at least once") + let membershipsHit = XCTestExpectation(description: "/memberships should be hit multiple times") + + //MARK: Key part + membershipsHit.expectedFulfillmentCount = 4 + + // 1. Configure dispatcher + let dispatcher: HttpClientTestDispatcher = { request in + if request.url.absoluteString.contains("/splitChanges") { + let json = IntegrationHelper.loadSplitChangeFileJson(name: "splitchanges_1", sourceClass: IntegrationHelper()) // send splitChanges with Segments + return TestDispatcherResponse(code: 200, data: Data(json!.utf8)) + } + + if request.url.absoluteString.contains("/memberships") { + segmentsHit.fulfill() + membershipsHit.fulfill() + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) + } + return TestDispatcherResponse(code: 200) + } + + // 2. Setup Factory, Network & Client + let testFactory = TestSplitFactory(userKey: userKey) + testFactory.createHttpClient(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + try testFactory.buildSdk(polling: true) + let client = testFactory.client + + client.on(event: .sdkReady) { + sdkReadyFired = true + sdkReady.fulfill() + } + + wait(for: [segmentsHit], timeout: 3) + XCTAssertEqual(sdkReadyFired, false) + + // 3. Test + wait(for: [sdkReady, membershipsHit], timeout: 20) + + // Cleanup + destroy(client) + } + + func testSdkAvoidsMembershipsIfNoSegmentsAreUsed() throws { + + var sdkReadyFired = false + let userKey = "test-user-key" + + let sdkReady = XCTestExpectation(description: "SDK should be ready") + let segmentsHit = XCTestExpectation(description: "/memberships should be hit at least once") + var membershipsHit = 0 + + // 1. Configure dispatcher + let dispatcher: HttpClientTestDispatcher = { request in + if request.url.absoluteString.contains("/splitChanges") { + let json = IntegrationHelper.loadSplitChangeFileJson(name: "splitschanges_no_segments", sourceClass: IntegrationHelper()) // send splitChanges wtihout Segments + return TestDispatcherResponse(code: 200, data: Data(json!.utf8)) + } + + if request.url.absoluteString.contains("/memberships") { + segmentsHit.fulfill() + membershipsHit += 1 + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) + } + + return TestDispatcherResponse(code: 200) + } + + // 2. Setup Factory, Network & Client + let testFactory = TestSplitFactory(userKey: userKey) + testFactory.createHttpClient(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + try testFactory.buildSdk(polling: true) + let client = testFactory.client + + client.on(event: .sdkReady) { + sdkReadyFired = true + sdkReady.fulfill() + } + + wait(for: [segmentsHit], timeout: 3) + XCTAssertEqual(sdkReadyFired, false) + + // Inverted expectation + let waitExp = XCTestExpectation(description: "Just waiting") + waitExp.isInverted = true + wait(for: [waitExp], timeout: 15) + + // MARK: Key part + XCTAssertEqual(membershipsHit, 1, "After 15 seconds it should hit /memberships just once") + + // Cleanup + destroy(client) + } + + func testSdkAvoidsMembershipsIfNoSegmentsAreUsedFromCache() throws { + + var sdkReadyFired = false + var cacheReadyFired = true + let sdkReady = XCTestExpectation(description: "SDK should be ready") + let cacheReadyExp = XCTestExpectation(description: "Cache should be ready") + let segmentsHit = XCTestExpectation(description: "/memberships should be hit at least once") + var membershipsHit = 0 + + // 1. Configure dispatcher + let dispatcher: HttpClientTestDispatcher = { request in + if request.url.absoluteString.contains("/splitChanges") { + let json = IntegrationHelper.loadSplitChangeFileJson(name: "splitschanges_no_segments", sourceClass: IntegrationHelper()) // send splitChanges wtihout Segments + return TestDispatcherResponse(code: 200, data: Data(json!.utf8)) + } + + if request.url.absoluteString.contains("/memberships") { + segmentsHit.fulfill() + membershipsHit += 1 + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) + } + + return TestDispatcherResponse(code: 200) + } + + // 2. Setup Factory, Network & Client + let splitConfig: SplitClientConfig = SplitClientConfig() + splitConfig.featuresRefreshRate = 1 + splitConfig.segmentsRefreshRate = 1 + splitConfig.impressionRefreshRate = 30 + splitConfig.sdkReadyTimeOut = 60000 + splitConfig.eventsPerPush = 10 + splitConfig.streamingEnabled = false + splitConfig.eventsQueueSize = 100 + splitConfig.eventsPushRate = 999999 + splitConfig.eventsFirstPushWindow = 999 + splitConfig.impressionsMode = "DEBUG" + splitConfig.serviceEndpoints = ServiceEndpoints.builder() + .set(sdkEndpoint: "localhost").set(eventsEndpoint: "localhost").build() + + let splitDatabase = TestingHelper.createTestDatabase(name: "ready_from_cache_test") + let savedSplit = SplitTestHelper.newSplitWithMatcherType("splits_segments", .allKeys) + splitDatabase.splitDao.syncInsertOrUpdate(split: savedSplit) + + let userKey = "test-user-key" + let key: Key = Key(matchingKey: userKey, bucketingKey: nil) + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + let builder = DefaultSplitFactoryBuilder() + + _ = builder.setTestDatabase(splitDatabase) + _ = builder.setHttpClient(httpClient) + var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build() + let client = factory?.client + + client?.on(event: .sdkReady) { + sdkReadyFired = true + sdkReady.fulfill() + } + + client?.on(event: .sdkReadyFromCache) { + cacheReadyExp.fulfill() + cacheReadyFired = true + } + + wait(for: [segmentsHit], timeout: 3) + XCTAssertEqual(sdkReadyFired, false) + + wait(for: [cacheReadyExp, sdkReady], timeout: 3) + + // MARK: Key part + let waitExp = XCTestExpectation(description: "Just waiting") + waitExp.isInverted = true // Inverted expectation + wait(for: [waitExp], timeout: 10) + + XCTAssertEqual(membershipsHit, 1, "After 15 seconds it should hit /memberships just once") + + // Cleanup + if let client = client { + destroy(client) + } + } + + func testSdkHitsMembershipsIfSegmentsAreUsedFromCache() throws { + + var sdkReadyFired = false + var cacheReadyFired = true + let sdkReady = XCTestExpectation(description: "SDK should be ready") + let cacheReadyExp = XCTestExpectation(description: "Cache should be ready") + let segmentsHit = XCTestExpectation(description: "/memberships should be hit at least once") + var membershipsHit = 0 + + // 1. Configure dispatcher + let dispatcher: HttpClientTestDispatcher = { request in + if request.url.absoluteString.contains("/splitChanges") { + let json = IntegrationHelper.loadSplitChangeFileJson(name: "splitschanges_no_segments", sourceClass: IntegrationHelper()) // splitChanges wtih no Segments + return TestDispatcherResponse(code: 200, data: Data(json!.utf8)) + } + + if request.url.absoluteString.contains("/memberships") { + segmentsHit.fulfill() + membershipsHit += 1 + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.mySegments(names: ["", ""]).utf8)) + } + + return TestDispatcherResponse(code: 200) + } + + // 2. Setup Factory, Network & Client + let splitConfig: SplitClientConfig = SplitClientConfig() + splitConfig.featuresRefreshRate = 1 + splitConfig.segmentsRefreshRate = 1 + splitConfig.impressionRefreshRate = 30 + splitConfig.sdkReadyTimeOut = 60000 + splitConfig.eventsPerPush = 10 + splitConfig.streamingEnabled = false + splitConfig.eventsQueueSize = 100 + splitConfig.eventsPushRate = 999999 + splitConfig.eventsFirstPushWindow = 999 + splitConfig.impressionsMode = "DEBUG" + splitConfig.serviceEndpoints = ServiceEndpoints.builder() + .set(sdkEndpoint: "localhost").set(eventsEndpoint: "localhost").build() + + let splitDatabase = TestingHelper.createTestDatabase(name: "ready_from_cache_test") + splitDatabase.generalInfoDao.update(info: .segmentsInUse, longValue: 1) + splitDatabase.generalInfoDao.update(info: .flagsSpec, stringValue: "1.3") + let savedSplit = SplitTestHelper.newSplitWithMatcherType("splits_segments", .inSegment) + splitDatabase.splitDao.syncInsertOrUpdate(split: savedSplit) + + let userKey = "test-user-key" + let key: Key = Key(matchingKey: userKey, bucketingKey: nil) + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + let builder = DefaultSplitFactoryBuilder() + + _ = builder.setTestDatabase(splitDatabase) + _ = builder.setHttpClient(httpClient) + var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build() + let client = factory?.client + + client?.on(event: .sdkReady) { + sdkReadyFired = true + sdkReady.fulfill() + } + + client?.on(event: .sdkReadyFromCache) { + cacheReadyExp.fulfill() + cacheReadyFired = true + } + + wait(for: [segmentsHit, cacheReadyExp, sdkReady], timeout: 4) + + // MARK: Key part + let waitExp = XCTestExpectation(description: "Just waiting") + waitExp.isInverted = true // Inverted expectation + wait(for: [waitExp], timeout: 10) + + XCTAssertGreaterThan(membershipsHit, 2, "After 15 seconds, if segments are used, SDK should hit /memberships many times") + + // Cleanup + if let client = client { + destroy(client) + } + } + + func testSdkRestartMembershipsSyncIfNewFlag() throws { + + var sdkReadyFired = false + var cacheReadyFired = true + let sdkReady = XCTestExpectation(description: "SDK should be ready") + let cacheReadyExp = XCTestExpectation(description: "Cache should be ready") + let segmentsHit = XCTestExpectation(description: "/memberships should be hit at least once") + var membershipsHit = 0 + + var json = IntegrationHelper.loadSplitChangeFileJson(name: "splitschanges_no_segments", sourceClass: IntegrationHelper()) // no Segments + + // 1. Configure dispatcher + let dispatcher: HttpClientTestDispatcher = { request in + if request.url.absoluteString.contains("/splitChanges") { + return TestDispatcherResponse(code: 200, data: Data(json!.utf8)) + } + + if request.url.absoluteString.contains("/memberships") { + segmentsHit.fulfill() + membershipsHit += 1 + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) + } + + return TestDispatcherResponse(code: 200) + } + + // 2. Setup Factory, Network & Client + let splitConfig: SplitClientConfig = SplitClientConfig() + splitConfig.featuresRefreshRate = 2 + splitConfig.segmentsRefreshRate = 2 + splitConfig.impressionRefreshRate = 30 + splitConfig.sdkReadyTimeOut = 60000 + splitConfig.eventsPerPush = 10 + splitConfig.streamingEnabled = false + splitConfig.eventsQueueSize = 100 + splitConfig.eventsPushRate = 999999 + splitConfig.eventsFirstPushWindow = 999 + splitConfig.impressionsMode = "DEBUG" + splitConfig.serviceEndpoints = ServiceEndpoints.builder() + .set(sdkEndpoint: "localhost").set(eventsEndpoint: "localhost").build() + + let splitDatabase = TestingHelper.createTestDatabase(name: "ready_from_cache_test") + + let userKey = "test-user-key" + let key: Key = Key(matchingKey: userKey, bucketingKey: nil) + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + let builder = DefaultSplitFactoryBuilder() + + _ = builder.setTestDatabase(splitDatabase) + _ = builder.setHttpClient(httpClient) + var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build() + let client = factory?.client + + client?.on(event: .sdkReady) { + sdkReadyFired = true + sdkReady.fulfill() + } + + client?.on(event: .sdkReadyFromCache) { + cacheReadyExp.fulfill() + cacheReadyFired = true + } + + wait(for: [segmentsHit], timeout: 3) + XCTAssertEqual(sdkReadyFired, false) + + wait(for: [cacheReadyExp, sdkReady], timeout: 4) + + // MARK: Key part + var waitExp = XCTestExpectation(description: "Just waiting") + waitExp.isInverted = true // Inverted expectation + wait(for: [waitExp], timeout: 5) + XCTAssertEqual(membershipsHit, 1, "After some time, if segments are not used, SDK shouldn't hit /memberships") + + // MARK: Key part 2 + json = IntegrationHelper.loadSplitChangeFileJson(name: "splitchanges_1", sourceClass: IntegrationHelper()) // splitChanges, now WITH Segments + + waitExp = XCTestExpectation(description: "Just waiting") + waitExp.isInverted = true // Inverted expectation + wait(for: [waitExp], timeout: 5) + XCTAssertGreaterThan(membershipsHit, 2, "If new flags with segments arrive, the mechanism should be restarted and SDK should hit /memberships many times again") + + // Cleanup + if let client = client { + destroy(client) + } + } + + func testSDKReparsesDatabaseIfSegmentsInUseIsNull() throws { + + var sdkReadyFired = false + let sdkReady = XCTestExpectation(description: "SDK should be ready") + + var json = IntegrationHelper.loadSplitChangeFileJson(name: "splitschanges_no_segments", sourceClass: IntegrationHelper()) // no Segments + + // 1. Configure dispatcher + let dispatcher: HttpClientTestDispatcher = { request in + if request.url.absoluteString.contains("/splitChanges") { + var json = IntegrationHelper.loadSplitChangeFileJson(name: "splitschanges_no_segments", sourceClass: IntegrationHelper()) // no Segments + return TestDispatcherResponse(code: 200, data: Data(json!.utf8)) + } + + if request.url.absoluteString.contains("/memberships") { + return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptyMySegments.utf8)) + } + + return TestDispatcherResponse(code: 200) + } + + // 2. Setup Factory, Network & Client + let splitConfig: SplitClientConfig = SplitClientConfig() + splitConfig.featuresRefreshRate = 5 + splitConfig.segmentsRefreshRate = 5 + splitConfig.impressionRefreshRate = 30 + splitConfig.sdkReadyTimeOut = 60000 + splitConfig.eventsPerPush = 10 + splitConfig.streamingEnabled = false + splitConfig.eventsQueueSize = 100 + splitConfig.eventsPushRate = 999999 + splitConfig.eventsFirstPushWindow = 999 + splitConfig.impressionsMode = "DEBUG" + splitConfig.serviceEndpoints = ServiceEndpoints.builder() + .set(sdkEndpoint: "localhost").set(eventsEndpoint: "localhost").build() + + // Mock data + let splitDatabase = TestingHelper.createTestDatabase(name: "ready_from_cache_test") + splitDatabase.generalInfoDao.update(info: .flagsSpec, stringValue: "1.3") + let savedSplit1 = SplitTestHelper.newSplitWithMatcherType("splits_segments1", .inSegment, parsed: false) + let savedSplit2 = SplitTestHelper.newSplitWithMatcherType("splits_segments2", .allKeys, parsed: false) + let savedSplit3 = SplitTestHelper.newSplitWithMatcherType("splits_segments3", .inSegment, parsed: false) + let savedSplit4 = SplitTestHelper.newSplitWithMatcherType("splits_segments4", .allKeys, parsed: false) + let savedSplit5 = SplitTestHelper.newSplitWithMatcherType("splits_segments5", .inSegment, parsed: false) + let savedSplit6 = SplitTestHelper.newSplitWithMatcherType("splits_segments6", .inLargeSegment, parsed: false) + splitDatabase.splitDao.syncInsertOrUpdate(split: savedSplit1) + splitDatabase.splitDao.syncInsertOrUpdate(split: savedSplit2) + splitDatabase.splitDao.syncInsertOrUpdate(split: savedSplit3) + splitDatabase.splitDao.syncInsertOrUpdate(split: savedSplit4) + splitDatabase.splitDao.syncInsertOrUpdate(split: savedSplit5) + splitDatabase.splitDao.syncInsertOrUpdate(split: savedSplit6) + + let userKey = "test-user-key" + let key: Key = Key(matchingKey: userKey, bucketingKey: nil) + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + let builder = DefaultSplitFactoryBuilder() + + _ = builder.setTestDatabase(splitDatabase) + _ = builder.setHttpClient(httpClient) + var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build() + let client = factory?.client + + client?.on(event: .sdkReady) { + sdkReadyFired = true + sdkReady.fulfill() + } + wait(for: [sdkReady], timeout: 4) + + // MARK: Key part + XCTAssertEqual(splitDatabase.generalInfoDao.longValue(info: .segmentsInUse), 4) + + // Cleanup + if let client = client { + destroy(client) + } + } func testMySegmentsUpdateBounded() throws { try mySegmentsUpdateBoundedTest(type: .mySegmentsUpdate) @@ -346,4 +781,12 @@ class MySegmentUpdateTest: XCTestCase { msg = notificationTemplate.replacingOccurrences(of: kDataField, with: msg) streamingBinding?.push(message: msg) } + + fileprivate func destroy(_ client: SplitClient) { + let semaphore = DispatchSemaphore(value: 0) + client.destroy { + semaphore.signal() + } + semaphore.wait() + } } diff --git a/SplitTests/Resources/splitschanges_no_segments.json b/SplitTests/Resources/splitschanges_no_segments.json new file mode 100644 index 000000000..6feaf7805 --- /dev/null +++ b/SplitTests/Resources/splitschanges_no_segments.json @@ -0,0 +1,3039 @@ +{"ff": { + "d":[ + { + "trafficTypeName":"account", + "name":"NEW_FACUNDO_TEST", + "trafficAllocation":59, + "trafficAllocationSeed":-2108186082, + "seed":-1947050785, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1506703262916, + "algo":2, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "bla" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + }, + { + "treatment":"visa", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_testing", + "trafficAllocation":100, + "trafficAllocationSeed":527505678, + "seed":-1716462249, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1506440189077, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_SPLIT_TREATMENT", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":{ + "split":"test_definition_as_of", + "treatments":[ + "on" + ] + }, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_SPLIT_TREATMENT", + "negate":true, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":{ + "split":"Identify_UI", + "treatments":[ + "on" + ] + }, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + }, + { + "treatment":"off", + "size":0 + } + ], + "label":"in split test_definition_as_of treatment [on] and not in split Identify_UI treatment [on]" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_SPLIT_TREATMENT", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":{ + "split":"test_definition_as_of", + "treatments":[ + "off" + ] + }, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in split test_definition_as_of treatment [off]" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_testing222", + "trafficAllocation":100, + "trafficAllocationSeed":-397360967, + "seed":1058132210, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1505162627437, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + }, + { + "treatment":"test222", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_a_new_split_2", + "trafficAllocation":99, + "trafficAllocationSeed":-1349440646, + "seed":-1536389703, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1505161671620, + "algo":2, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "adil", + "bb", + "bbb", + "dd3c0800-30f1-11e7-ba78-12395d4a9634", + "pato", + "tito" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"segment2" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"test_copy" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":52 + }, + { + "treatment":"off", + "size":48 + }, + { + "treatment":"testo", + "size":0 + } + ], + "label":"in segment all" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":"asda" + }, + "matcherType":"STARTS_WITH", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "ee", + "aa" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_L", + "negate":true, + "userDefinedSegmentMatcherData":{ + "segmentName":"segment2" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + }, + { + "treatment":"testo", + "size":0 + } + ], + "label":"asda starts with [ee, aa] and not in segment segment2" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":"pp" + }, + "matcherType":"PART_OF_SET", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "pato", + "adil" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + }, + { + "treatment":"off", + "size":0 + }, + { + "treatment":"testo", + "size":0 + } + ], + "label":"pp part of [pato, adil]" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":"eee" + }, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "1", + "2", + "trevorrr" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":20 + }, + { + "treatment":"off", + "size":80 + }, + { + "treatment":"testo", + "size":0 + } + ], + "label":"eee in list [1, 2, ...]" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_test_string_without_attr", + "trafficAllocation":100, + "trafficAllocationSeed":-782597068, + "seed":-1682478887, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1504805281437, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"Segment3" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "something" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment Segment3 and in list [something]" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_OldTest", + "trafficAllocation":100, + "trafficAllocationSeed":217539832, + "seed":52164426, + "status":"ACTIVE", + "killed":true, + "defaultTreatment":"off", + "changeNumber":1504206031141, + "algo":2, + "configurations": { + "off": "{\"f1\":\"v1\"}" + }, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"sample-segment" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"demo" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"employees" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"sample-segment" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"user", + "attribute":"fsdfsd" + }, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "a", + "b", + "c", + "d" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"user", + "attribute":"asdasdasd" + }, + "matcherType":"STARTS_WITH", + "negate":true, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "asdad", + "sa", + "das" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":10 + }, + { + "treatment":"off", + "size":90 + } + ], + "label":"in segment sample-segment and fsdfsd in list [a, b, ...] and asdasdasd does not start with [asdad, sa, ...]" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_Test_Save_1", + "trafficAllocation":100, + "trafficAllocationSeed":-257595325, + "seed":-665945237, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1503956389520, + "algo":2, + "configurations": { + "off": "{\"f1\":\"v1\"}" + }, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "1", + "12", + "123", + "23" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "asd" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"Segment3" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + }, + { + "treatment":"v1", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_TEST", + "trafficAllocation":100, + "trafficAllocationSeed":-673356676, + "seed":-511119211, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1503942404754, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + }, + { + "treatment":"off", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_benchmark_jw_1", + "trafficAllocation":100, + "trafficAllocationSeed":987354894, + "seed":1292874260, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1503356075822, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":"atrib" + }, + "matcherType":"BETWEEN", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":{ + "dataType":"NUMBER", + "start":1474990940, + "end":1474990949 + }, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":95 + }, + { + "treatment":"off", + "size":5 + } + ], + "label":"atrib between 1474990940 and 1474990949" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":90 + }, + { + "treatment":"off", + "size":10 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_nico_tests", + "trafficAllocation":100, + "trafficAllocationSeed":1409699192, + "seed":-1997241870, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1501791316810, + "algo":2, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "nico_test_browser__key" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"employees" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + }, + { + "treatment":"off", + "size":0 + } + ], + "label":"in segment employees" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_testo2222", + "trafficAllocation":100, + "trafficAllocationSeed":1164474086, + "seed":1270508512, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1501012403336, + "algo":2, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "aasd" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"segment2" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "ddddd" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"Segment3" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "ppp", + "ppppp" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"pesto", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_L", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"test_copy" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"pesto", + "size":100 + } + ], + "label":"whitelisted segment" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":0 + }, + { + "treatment":"on", + "size":100 + }, + { + "treatment":"pesto", + "size":0 + }, + { + "treatment":"arse", + "size":0 + }, + { + "treatment":"zzzzick", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_Tagging", + "trafficAllocation":100, + "trafficAllocationSeed":1910132597, + "seed":-311493896, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1500590774768, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":50 + }, + { + "treatment":"off", + "size":50 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_Welcome_Page_UI", + "trafficAllocation":100, + "trafficAllocationSeed":1848523960, + "seed":1608586361, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1500577256901, + "algo":2, + "configurations": { + "off": "{\"the_emojis\":\"\\uD83D\\uDE01 -- áéíóúöÖüÜÏëç\"}" + }, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"test", + "name":"NEW_pato_test_3", + "trafficAllocation":100, + "trafficAllocationSeed":458647735, + "seed":95677506, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1500510847849, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"test", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_testo23", + "trafficAllocation":100, + "trafficAllocationSeed":-689658216, + "seed":1711156051, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1500064145947, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":"sdsd" + }, + "matcherType":"EQUAL_TO_BOOLEAN", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":true, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"sdsd is true" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_testo909090", + "trafficAllocation":100, + "trafficAllocationSeed":-1196467266, + "seed":-1998101827, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1500039488369, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":"a" + }, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "a" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"account", + "attribute":"v" + }, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "a" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"account", + "attribute":"asdadas" + }, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "a", + "b", + "c", + "d" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"account", + "attribute":"sds" + }, + "matcherType":"CONTAINS_STRING", + "negate":true, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "a", + "c", + "d" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"account", + "attribute":"xcvxv" + }, + "matcherType":"EQUAL_TO", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":{ + "dataType":"NUMBER", + "value":122 + }, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"a in list [a] and v in list [a] and asdadas in list [a, b, ...] and sds does not contain [a, c, ...] and xcvxv = 122" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + }, + { + "treatment":"off", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_testo22", + "trafficAllocation":100, + "trafficAllocationSeed":1223277820, + "seed":-1152948537, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1499721434259, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_test-net", + "trafficAllocation":100, + "trafficAllocationSeed":-2038196969, + "seed":-862203077, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1499718635999, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_test_dep_2", + "trafficAllocation":100, + "trafficAllocationSeed":-806171485, + "seed":922684950, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1499707910800, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_SPLIT_TREATMENT", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":{ + "split":"Identify_UI", + "treatments":[ + "on" + ] + }, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in split Identify_UI treatment [on]" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_SPLIT_TREATMENT", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":{ + "split":"Definition_As_Of_Clickable_UI", + "treatments":[ + "off" + ] + }, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":50 + }, + { + "treatment":"off", + "size":50 + } + ], + "label":"in split Definition_As_Of_Clickable_UI treatment [off]" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_Definition_As_Of_Clickable_UI", + "trafficAllocation":100, + "trafficAllocationSeed":-198035199, + "seed":-151947071, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1498168847351, + "algo":2, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "tito", + "trevor" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_Identify_UI", + "trafficAllocation":100, + "trafficAllocationSeed":-139516103, + "seed":1543172523, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1498078888450, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + }, + { + "treatment":"off", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_test_definition_as_of", + "trafficAllocation":100, + "trafficAllocationSeed":1025823325, + "seed":-554248124, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1497289730024, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negatee":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_Test-jw-go", + "trafficAllocation":100, + "trafficAllocationSeed":768122971, + "seed":1539205707, + "status":"ACTIVE", + "defaultTreatment":"off", + "changeNumber":1496339112852, + "algo":2, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "test1" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_benchmark_jw_7", + "trafficAllocation":100, + "trafficAllocationSeed":-1340337178, + "seed":-1091938685, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1494593464885, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_benchmark_jw_6", + "trafficAllocation":100, + "trafficAllocationSeed":-1202331834, + "seed":-48445256, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1494593448028, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_benchmark_jw_5", + "trafficAllocation":100, + "trafficAllocationSeed":2119994290, + "seed":-227092192, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1494593428034, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_benchmark_jw_4", + "trafficAllocation":100, + "trafficAllocationSeed":1066635158, + "seed":-850704283, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1494593412226, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_benchmark_jw_3", + "trafficAllocation":100, + "trafficAllocationSeed":1252392550, + "seed":971538037, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1494593352077, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_benchmark_jw_2", + "trafficAllocation":100, + "trafficAllocationSeed":-285565213, + "seed":-1992295819, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1494593336752, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"user", + "name":"NEW_broken_split", + "trafficAllocation":100, + "trafficAllocationSeed":-285565213, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1494593336752 + }, + { + "trafficTypeName":"account", + "name":"NEW_TEST_SETS_1", + "trafficAllocation":59, + "trafficAllocationSeed":-2108186082, + "seed":-1947050785, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1506703262916, + "algo":2, + "sets": ["set1", "set2"], + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "bla" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off1", + "size":100 + }, + { + "treatment":"visa", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_TEST_SETS_2", + "trafficAllocation":59, + "trafficAllocationSeed":-2108186082, + "seed":-1947050785, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off2", + "changeNumber":1506703262916, + "algo":2, + "configurations": { + "off2": "{\"f1\":\"v1\"}" + }, + "sets": ["set2", "set3"], + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "bla" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off2", + "size":100 + }, + { + "treatment":"visa", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"NEW_TEST_SETS_3", + "trafficAllocation":59, + "trafficAllocationSeed":-2108186082, + "seed":-1947050785, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off3", + "changeNumber":1506703262916, + "algo":2, + "sets": ["set10", "set20"], + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "bla" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off3", + "size":100 + }, + { + "treatment":"visa", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "name": "always_on_if_prerequisite", + "trafficTypeName": "user", + "trafficAllocation": 100, + "trafficAllocationSeed": 1828377380, + "changeNumber": 5, + "seed": -790401604, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "prerequisites": [ + { + "n": "rbs_test_flag", + "ts": [ + "v1" + ] + } + ], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ], + "label": "always_on_if_prerequisite label" + } + ] + }, + { + "name": "rbs_test_flag", + "trafficTypeName": "user", + "trafficAllocation": 100, + "trafficAllocationSeed": 1828377380, + "seed": -286617921, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "IN_RULE_BASED_L", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "test_rule_based_segment" + } + } + ] + }, + "partitions": [ + { + "treatment": "v1", + "size": 100 + }, + { + "treatment": "v2", + "size": 0 + } + ], + "label": "in rule based segment test_rule_based_segment" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "ALL_KEYS", + "negate": false + } + ] + }, + "partitions": [ + { + "treatment": "v1", + "size": 0 + }, + { + "treatment": "v2", + "size": 100 + } + ], + "label": "default rule" + } + ], + "configurations": {}, + "sets": [], + "impressionsDisabled": false + } + ], + "s": 1506703262916, + "t":1506703262916 +}, "rbs": {"s":-1, "t":-1, "d": [ + { + "name": "test_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded": { + "keys": [ + "mauro@split.io", + "gaston@split.io" + ], + "segments": [] + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + }] + }] +}} diff --git a/SplitTests/Storage/GeneralInfoDaoTests.swift b/SplitTests/Storage/GeneralInfoDaoTests.swift index 65c563a86..017c8daba 100644 --- a/SplitTests/Storage/GeneralInfoDaoTests.swift +++ b/SplitTests/Storage/GeneralInfoDaoTests.swift @@ -54,6 +54,15 @@ class GeneralInfoDaoTest: XCTestCase { XCTAssertEqual(1, v1) XCTAssertEqual(2, v2) } + + func testCreateUpdateSegmentsInUse() { + let data: Int64 = 13 + + generalInfoDao.update(info: .segmentsInUse, longValue: data) + let segmentsInUse = generalInfoDao.longValue(info: .segmentsInUse) + + XCTAssertEqual(data, segmentsInUse) + } override func tearDown() { } diff --git a/SplitTests/Storage/MyLargeSegmentsStorageTests.swift b/SplitTests/Storage/MyLargeSegmentsStorageTests.swift index ee6a69598..e3d98e040 100644 --- a/SplitTests/Storage/MyLargeSegmentsStorageTests.swift +++ b/SplitTests/Storage/MyLargeSegmentsStorageTests.swift @@ -15,12 +15,14 @@ class MyLargeSegmentsStorageTests: XCTestCase { var persistentStorage: PersistentMySegmentsStorageMock! var mySegmentsStorage: MySegmentsStorage! + var generalInfoStorage: GeneralInfoStorageMock! var userKey = "dummyKey" var dummyChange = SegmentChange(segments: ["s1", "s2", "s3"], changeNumber: 100) override func setUp() { persistentStorage = PersistentMySegmentsStorageMock() - mySegmentsStorage = MyLargeSegmentsStorage(persistentStorage: persistentStorage) + generalInfoStorage = GeneralInfoStorageMock() + mySegmentsStorage = MyLargeSegmentsStorage(persistentStorage: persistentStorage, generalInfoStorage: generalInfoStorage) } func testNoLoaded() { @@ -148,6 +150,11 @@ class MyLargeSegmentsStorageTests: XCTestCase { XCTAssertEqual(100, cn1) XCTAssertEqual(200, cn2) } + + func testIsUsingSegments() { + generalInfoStorage.setSegmentsInUse(5) + XCTAssertTrue(mySegmentsStorage.isUsingSegments()) + } func testClearAll() { let otherKey = "otherKey" diff --git a/SplitTests/Storage/MySegmentsStorageTests.swift b/SplitTests/Storage/MySegmentsStorageTests.swift index fe099931d..10fe348db 100644 --- a/SplitTests/Storage/MySegmentsStorageTests.swift +++ b/SplitTests/Storage/MySegmentsStorageTests.swift @@ -15,12 +15,14 @@ class MySegmentsStorageTests: XCTestCase { var persistentStorage: PersistentMySegmentsStorageMock! var mySegmentsStorage: MySegmentsStorage! + var generalInfoStorage: GeneralInfoStorageMock! var userKey = "dummyKey" var dummySegments = SegmentChange(segments: ["s1", "s2", "s3"]) override func setUp() { persistentStorage = PersistentMySegmentsStorageMock() - mySegmentsStorage = DefaultMySegmentsStorage(persistentMySegmentsStorage: persistentStorage) + generalInfoStorage = GeneralInfoStorageMock() + mySegmentsStorage = DefaultMySegmentsStorage(persistentMySegmentsStorage: persistentStorage, generalInfoStorage: generalInfoStorage) } func testNoLoaded() { @@ -120,6 +122,20 @@ class MySegmentsStorageTests: XCTestCase { XCTAssertEqual(-1, cn1) XCTAssertEqual(-1, cn2) } + + func testIsUsingSegments() { + generalInfoStorage.setSegmentsInUse(0) + XCTAssertEqual(mySegmentsStorage.isUsingSegments(), false) + + generalInfoStorage.setSegmentsInUse(5) + XCTAssertEqual(mySegmentsStorage.isUsingSegments(), true) + + generalInfoStorage.setSegmentsInUse(-1) + XCTAssertEqual(mySegmentsStorage.isUsingSegments(), false) + + generalInfoStorage.setSegmentsInUse(6) + XCTAssertEqual(mySegmentsStorage.isUsingSegments(), true) + } func testClearAll() { let otherKey = "otherKey" diff --git a/SplitTests/Storage/RuleBasedSegmentStorageTest.swift b/SplitTests/Storage/RuleBasedSegmentStorageTest.swift index 497e15451..f2fa32185 100644 --- a/SplitTests/Storage/RuleBasedSegmentStorageTest.swift +++ b/SplitTests/Storage/RuleBasedSegmentStorageTest.swift @@ -14,6 +14,7 @@ class RuleBasedSegmentStorageTest: XCTestCase { private var persistentStorageStub: PersistentRuleBasedSegmentsStorageStub! private var ruleBasedSegmentsStorage: DefaultRuleBasedSegmentsStorage! + private var noLoadedRbs: DefaultRuleBasedSegmentsStorage? override func setUp() { ruleBasedSegmentsStorage = DefaultRuleBasedSegmentsStorage( @@ -24,8 +25,25 @@ class RuleBasedSegmentStorageTest: XCTestCase { override func tearDown() { ruleBasedSegmentsStorage.clear() + noLoadedRbs?.clear() super.tearDown() } + + func testLazyParsing() { + noLoadedRbs = DefaultRuleBasedSegmentsStorage( + persistentStorage: createPersistentStorageStub(isParsed: false) + ) + + noLoadedRbs?.loadLocal() + + XCTAssertNotNil(noLoadedRbs) + var segmentToCheck = noLoadedRbs!.getInMemorySegments().value(forKey: "segment_1")! + XCTAssertEqual(segmentToCheck.isParsed, false, "segment_1 shouldn't be parsed") + + _ = noLoadedRbs?.get(segmentName: "segment_2") + segmentToCheck = noLoadedRbs!.getInMemorySegments().value(forKey: "segment_2")! + XCTAssertEqual(segmentToCheck.isParsed, true, "segment_2 should be parsed") + } func testInitialization() { XCTAssertEqual(ruleBasedSegmentsStorage.changeNumber, 123) @@ -275,30 +293,73 @@ class RuleBasedSegmentStorageTest: XCTestCase { XCTAssertTrue(persistentStorageStub.clearCalled) } + + func testSegmentsInUseCount() { + let segment1 = createSegmentWithMatcher("segment", .between) + let segment2 = createSegmentWithMatcher("segment2", .inSegment) + let segment3 = createSegmentWithMatcher("segment3", .inLargeSegment) + let segment4 = createSegmentWithMatcher("segment4", .inSegment) + let segment5 = createSegmentWithMatcher("segment5", .endsWith) + + // 1. Counter should be 3 (ignore the other matcherTypes) + _ = ruleBasedSegmentsStorage.update(toAdd: Set([segment1, segment2, segment3, segment4, segment5]), toRemove: [], changeNumber: 123) + XCTAssertEqual(ruleBasedSegmentsStorage.segmentsInUse, 3) + + // 2 + segment1.status = .archived // Archive of Segments with other matcherTypes should be ignored.. + segment2.status = .archived // ..and known Segments being archived should decrease the counter + segment3.status = .archived + _ = ruleBasedSegmentsStorage.update(toAdd: Set([]), toRemove: [segment1, segment2, segment3], changeNumber: 1230) + + XCTAssertEqual(ruleBasedSegmentsStorage.segmentsInUse, 1) + } + - private func createPersistentStorageStub() -> PersistentRuleBasedSegmentsStorageStub { - let delegate = MockPersistentRuleBasedSegmentsStorage() + private func createPersistentStorageStub(isParsed: Bool = true) -> PersistentRuleBasedSegmentsStorageStub { + let delegate = MockPersistentRuleBasedSegmentsStorage(isParsed: isParsed) persistentStorageStub = PersistentRuleBasedSegmentsStorageStub(delegate: delegate) return persistentStorageStub } - private func createSegment(name: String, trafficType: String = "tt_default", status: Status = .active) -> RuleBasedSegment { + private func createSegment(name: String, trafficType: String = "tt_default", status: Status = .active, isParsed: Bool = true) -> RuleBasedSegment { let segment = RuleBasedSegment() segment.name = name segment.trafficTypeName = trafficType segment.status = status segment.changeNumber = Int64(Date.nowMillis()) - segment.isParsed = true + segment.isParsed = isParsed + return segment + } + + private func createSegmentWithMatcher(_ name: String, _ matcher: MatcherType, isParsed: Bool = true) -> RuleBasedSegment { + let segment = RuleBasedSegment() + segment.name = name + segment.conditions = [Condition()] + segment.conditions![0].matcherGroup = MatcherGroup() + segment.conditions![0].matcherGroup?.matchers = [Matcher()] + segment.conditions![0].matcherGroup?.matchers![0].matcherType = matcher + segment.trafficTypeName = "user" + segment.status = .active + segment.changeNumber = Int64(Date.nowMillis()) + segment.isParsed = isParsed return segment } } private class MockPersistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStorage { - private var segments = [ - createSegment(name: "segment_1", trafficType: "tt_1"), - createSegment(name: "segment_2", trafficType: "tt_2"), - createSegment(name: "segment_3", trafficType: "tt_3") - ] + + private let isParsed: Bool + var segments: [RuleBasedSegment] = [] + + init(isParsed: Bool = true) { + self.isParsed = isParsed + segments = [ + createSegment(name: "segment_1", trafficType: "tt_1", isParsed: isParsed), + createSegment(name: "segment_2", trafficType: "tt_2", isParsed: isParsed), + createSegment(name: "segment_3", trafficType: "tt_3", isParsed: isParsed) + ] + } + private var snapshotChangeNumber: Int64 = 123 func getSnapshot() -> RuleBasedSegmentsSnapshot { @@ -321,14 +382,30 @@ private class MockPersistentRuleBasedSegmentsStorage: PersistentRuleBasedSegment self.segments = segments self.snapshotChangeNumber = changeNumber } + + var segmentsInUse: Int64 = 0 + func getSegmentsInUse() -> Int64? { + segmentsInUse + } + + func setSegmentsInUse(_ segmentsInUse: Int64) { + self.segmentsInUse = segmentsInUse + } - private static func createSegment(name: String, trafficType: String) -> RuleBasedSegment { + private func createSegment(name: String, trafficType: String, isParsed: Bool = true) -> RuleBasedSegment { let segment = RuleBasedSegment() segment.name = name segment.trafficTypeName = trafficType segment.status = .active segment.changeNumber = 123 - segment.isParsed = true + segment.isParsed = isParsed + segment.json = """ + { + "name": "\(name)", + "trafficTypeName": "\(trafficType)", + "status": "ACTIVE" + } + """ return segment } } diff --git a/SplitTests/Storage/SplitsStorageTests.swift b/SplitTests/Storage/SplitsStorageTests.swift index b89e7cd19..748c2b779 100644 --- a/SplitTests/Storage/SplitsStorageTests.swift +++ b/SplitTests/Storage/SplitsStorageTests.swift @@ -19,6 +19,7 @@ class SplitsStorageTest: XCTestCase { var persistentStorage: PersistentSplitsStorageStub! var splitsStorage: SplitsStorage! + var noLoadedStorage: DefaultSplitsStorage? override func setUp() { persistentStorage = PersistentSplitsStorageStub() @@ -36,6 +37,22 @@ class SplitsStorageTest: XCTestCase { XCTAssertEqual(-1,changeNumber) XCTAssertEqual(-1, updateTimestamp) } + + func testLazyParsing() { + noLoadedStorage = DefaultSplitsStorage( + persistentSplitsStorage: createPersistentStorageStub(isParsed: false), flagSetsCache: FlagSetsCacheMock() + ) + + noLoadedStorage?.loadLocal() + + XCTAssertNotNil(noLoadedStorage) + var splitToCheck = noLoadedStorage!.getInMemorySplits().value(forKey: "split_1")! + XCTAssertEqual(splitToCheck.isCompletelyParsed, false, "Split_1 shouldn't be parsed") + + _ = noLoadedStorage?.get(name: "split_2") + splitToCheck = noLoadedStorage!.getInMemorySplits().value(forKey: "split_2")! + XCTAssertEqual(splitToCheck.isCompletelyParsed, true, "Split_2 should be parsed") + } func testLoaded() { @@ -296,6 +313,47 @@ class SplitsStorageTest: XCTestCase { XCTAssertTrue(resultOnAdd) XCTAssertFalse(resultOnNoChange) } + + func testSegmentsInUse() { + let split = SplitTestHelper.newSplitWithMatcherType("split", .inSegment) + let split2 = SplitTestHelper.newSplitWithMatcherType("split2", .inLargeSegment) + let split3 = SplitTestHelper.newSplitWithMatcherType("split3", .inLargeSegment) + let split4 = SplitTestHelper.newSplitWithMatcherType("split4", .inLargeSegment) + let split5 = SplitTestHelper.newSplitWithMatcherType("split5", .inLargeSegment) + let split6 = SplitTestHelper.newSplitWithMatcherType("split6", .inLargeSegment) + + persistentStorage.snapshot = getTestSnapshot() + splitsStorage.loadLocal() + + // 1. Check Segments count is in 0 + XCTAssertEqual(splitsStorage.segmentsInUse, 0) + + // 2. Add 6 Splits (1 not using Segments) + var processedChange = ProcessedSplitChange(activeSplits: [split, split2, split3, split4, newSplit(name: "added"), split5], + archivedSplits: [], + changeNumber: 999, updateTimestamp: 888) + + _ = splitsStorage.update(splitChange: processedChange) + XCTAssertEqual(splitsStorage.segmentsInUse, 5) // One should have been ignored, so 5 + XCTAssertTrue(persistentStorage.updateCalled) + XCTAssertTrue(persistentStorage.getSegmentsInUseCalled) + + // 3. Add 2 previously added (should be ignored by the counter), and a new one + processedChange = ProcessedSplitChange(activeSplits: [split, split2, split6], + archivedSplits: [], + changeNumber: 9999, updateTimestamp: 8888) + + _ = splitsStorage.update(splitChange: processedChange) + XCTAssertEqual(splitsStorage.segmentsInUse, 6) // So, count should be 6 + + // 4. Remove 2 + processedChange = ProcessedSplitChange(activeSplits: [], + archivedSplits: [split2, split], + changeNumber: 99999, updateTimestamp: 88888) + + _ = splitsStorage.update(splitChange: processedChange) + XCTAssertEqual(splitsStorage.segmentsInUse, 4) // So, count should be 4 + } func testUnsupportedMatcherHasDefaultCondition() { let split = unsupportedMatcherSplit() @@ -356,4 +414,92 @@ class SplitsStorageTest: XCTestCase { return Split(name: "feature_flag_for_test", trafficType: "user", status: Status.active, sets: [], json: SplitTestHelper.getUnsupportedMatcherSplitJson(sourceClass: self)!) } + + fileprivate func createPersistentStorageStub(isParsed: Bool = true) -> PersistentSplitsStorageStub { + let delegate = MockPersistentSplitsSegmentsStorage(isParsed: isParsed) + return PersistentSplitsStorageStub(delegate: delegate) + } +} + +private class MockPersistentSplitsSegmentsStorage: PersistentSplitsStorage { + + private let isParsed: Bool + private let segmensInUse: Int64 = 0 + var splits: [Split] = [] + + init(isParsed: Bool = true) { + self.isParsed = isParsed + splits = [ + createSplit(name: "split_1", trafficType: "tt_1", parsed: isParsed), + createSplit(name: "split_2", trafficType: "tt_2", parsed: isParsed), + createSplit(name: "split_3", trafficType: "tt_3", parsed: isParsed) + ] + } + + private var snapshotChangeNumber: Int64 = 123 + + func getSplitsSnapshot() -> SplitsSnapshot { + SplitsSnapshot(changeNumber: snapshotChangeNumber, splits: splits, updateTimestamp: 1200) + } + + func update(split: Split) { + // No-op for the mock + } + + func clear() { + // No-op for the mock + } + + func getChangeNumber() -> Int64 { + return snapshotChangeNumber + } + + func updateSnapshotData(splits: [Split], changeNumber: Int64) { + self.splits = splits + self.snapshotChangeNumber = changeNumber + } + + var segmentsInUse: Int64 = 0 + func getSegmentsInUse() -> Int64? { + segmentsInUse + } + + func setSegmentsInUse(_ segmentsInUse: Int64) { + self.segmentsInUse = segmentsInUse + } + + func update(splitChange: ProcessedSplitChange) { + // No-op for the mock + } + + func update(bySetsFilter: SplitFilter?) { + // No-op for the mock + } + + func update(segmentsInUse: Int64) { + // No-op for the mock + } + + func getBySetsFilter() -> SplitFilter? { + SplitFilter(type: .byName, values: [""]) + } + + func getUpdateTimestamp() -> Int64 { + 0 + } + + func getAll() -> [Split] { + splits + } + + func delete(splitNames: [String]) { + // No-op for the mock + } + + private func createSplit(name: String, trafficType: String, status: Status = .active, parsed: Bool = false) -> Split { + let split = SplitTestHelper.newSplit(name: name, trafficType: trafficType) + split.status = status + split.isCompletelyParsed = parsed + return split + } }