From 26e9abb8cc0186736332a87ed3b70d5b398581e5 Mon Sep 17 00:00:00 2001 From: Kaushal Kapasi Date: Thu, 3 Jul 2025 11:55:11 -0400 Subject: [PATCH 1/3] fix: add sdk version to cache keys for configs --- DevCycle/Models/Cache.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/DevCycle/Models/Cache.swift b/DevCycle/Models/Cache.swift index f840020..53e9c4f 100644 --- a/DevCycle/Models/Cache.swift +++ b/DevCycle/Models/Cache.swift @@ -18,9 +18,12 @@ protocol CacheServiceProtocol { class CacheService: CacheServiceProtocol { struct CacheKeys { + static let platform = PlatformDetails() + static let versionPrefix = "VERSION_\(platform.sdkVersion)" + static let anonUserId = "ANONYMOUS_USER_ID" - static let identifiedConfigKey = "IDENTIFIED_CONFIG" - static let anonymousConfigKey = "ANONYMOUS_CONFIG" + static let identifiedConfigKey = "\(versionPrefix).IDENTIFIED_CONFIG" + static let anonymousConfigKey = "\(versionPrefix).ANONYMOUS_CONFIG" static let userIdSuffix = ".USER_ID" static let expiryDateSuffix = ".EXPIRY_DATE" From 9202cf47e7e6428be441d54ce9727fadfd784bc1 Mon Sep 17 00:00:00 2001 From: Kaushal Kapasi Date: Thu, 3 Jul 2025 12:38:54 -0400 Subject: [PATCH 2/3] chore: cleanup cached configs if they do not match the current sdk version --- DevCycle/Models/Cache.swift | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/DevCycle/Models/Cache.swift b/DevCycle/Models/Cache.swift index 53e9c4f..a74f37b 100644 --- a/DevCycle/Models/Cache.swift +++ b/DevCycle/Models/Cache.swift @@ -18,15 +18,18 @@ protocol CacheServiceProtocol { class CacheService: CacheServiceProtocol { struct CacheKeys { - static let platform = PlatformDetails() - static let versionPrefix = "VERSION_\(platform.sdkVersion)" + static let versionPrefix = "VERSION_\(PlatformDetails().sdkVersion)." static let anonUserId = "ANONYMOUS_USER_ID" - static let identifiedConfigKey = "\(versionPrefix).IDENTIFIED_CONFIG" - static let anonymousConfigKey = "\(versionPrefix).ANONYMOUS_CONFIG" + static let identifiedConfig = "IDENTIFIED_CONFIG" + static let anonymousConfig = "ANONYMOUS_CONFIG" + static let userIdSuffix = ".USER_ID" static let expiryDateSuffix = ".EXPIRY_DATE" + static let identifiedConfigKey = "\(versionPrefix)\(identifiedConfig)" + static let anonymousConfigKey = "\(versionPrefix)\(anonymousConfig)" + // Legacy keys for cleanup static let legacyUser = "user" static let legacyConfig = "config" @@ -39,6 +42,7 @@ class CacheService: CacheServiceProtocol { init(configCacheTTL: Int = DEFAULT_CONFIG_CACHE_TTL) { self.configCacheTTL = configCacheTTL migrateLegacyCache() + clearDeprecatedCachedConfigs() } func setAnonUserId(anonUserId: String) { @@ -129,6 +133,24 @@ class CacheService: CacheServiceProtocol { return baseKey } + private func clearDeprecatedCachedConfigs() { + let deprecatedKeys: [String] = defaults.dictionaryRepresentation().keys.compactMap { key in + // Only include keys that contain one of these patterns + guard key.contains(CacheKeys.identifiedConfig) || key.contains(CacheKeys.anonymousConfig) else { + return nil + } + + return key.starts(with: CacheKeys.versionPrefix) ? nil : key + } + + for key in deprecatedKeys { + if defaults.object(forKey: key) != nil { + defaults.removeObject(forKey: key) + Log.debug("Cleaned up cached config: \(key)") + } + } + } + // MARK: - Legacy Cache Migration func migrateLegacyCache() { From f11080dcf408b039b29c4bdad8a5e2ac5978bcad Mon Sep 17 00:00:00 2001 From: Kaushal Kapasi Date: Thu, 3 Jul 2025 13:56:49 -0400 Subject: [PATCH 3/3] chore: update cache migration tests to use new cache key format for identified & anonymous configs --- DevCycle/Models/Cache.swift | 6 +- DevCycleTests/Models/DevCycleUserTest.swift | 80 +++++++++++++++------ 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/DevCycle/Models/Cache.swift b/DevCycle/Models/Cache.swift index a74f37b..9fb35a8 100644 --- a/DevCycle/Models/Cache.swift +++ b/DevCycle/Models/Cache.swift @@ -42,7 +42,6 @@ class CacheService: CacheServiceProtocol { init(configCacheTTL: Int = DEFAULT_CONFIG_CACHE_TTL) { self.configCacheTTL = configCacheTTL migrateLegacyCache() - clearDeprecatedCachedConfigs() } func setAnonUserId(anonUserId: String) { @@ -133,7 +132,7 @@ class CacheService: CacheServiceProtocol { return baseKey } - private func clearDeprecatedCachedConfigs() { + private func cleanupDeprecatedCachedConfigs() { let deprecatedKeys: [String] = defaults.dictionaryRepresentation().keys.compactMap { key in // Only include keys that contain one of these patterns guard key.contains(CacheKeys.identifiedConfig) || key.contains(CacheKeys.anonymousConfig) else { @@ -163,6 +162,9 @@ class CacheService: CacheServiceProtocol { // Clean up legacy config cache cleanupLegacyConfigCache() + + // Clean up config cache from other SDK versions + cleanupDeprecatedCachedConfigs() } private func cleanupLegacyUserCache() { diff --git a/DevCycleTests/Models/DevCycleUserTest.swift b/DevCycleTests/Models/DevCycleUserTest.swift index 468e854..229afe2 100644 --- a/DevCycleTests/Models/DevCycleUserTest.swift +++ b/DevCycleTests/Models/DevCycleUserTest.swift @@ -240,16 +240,17 @@ class DevCycleUserTest: XCTestCase { let identifiedUserId = "identified_user_123" let anonymousUserId = "anon_user_456" + let versionPrefix = "VERSION_\(PlatformDetails().sdkVersion)" let configData = "{\"variables\": {\"test\": \"value\"}}".data(using: .utf8) let fetchDate = Int(Date().timeIntervalSince1970) - defaults.set(configData, forKey: "IDENTIFIED_CONFIG") - defaults.set(identifiedUserId, forKey: "IDENTIFIED_CONFIG.USER_ID") - defaults.set(fetchDate, forKey: "IDENTIFIED_CONFIG.FETCH_DATE") + defaults.set(configData, forKey: "\(versionPrefix).IDENTIFIED_CONFIG") + defaults.set(identifiedUserId, forKey: "\(versionPrefix).IDENTIFIED_CONFIG.USER_ID") + defaults.set(fetchDate, forKey: "\(versionPrefix).IDENTIFIED_CONFIG.FETCH_DATE") - defaults.set(configData, forKey: "ANONYMOUS_CONFIG") - defaults.set(anonymousUserId, forKey: "ANONYMOUS_CONFIG.USER_ID") - defaults.set(fetchDate, forKey: "ANONYMOUS_CONFIG.FETCH_DATE") + defaults.set(configData, forKey: "\(versionPrefix).ANONYMOUS_CONFIG") + defaults.set(anonymousUserId, forKey: "\(versionPrefix).ANONYMOUS_CONFIG.USER_ID") + defaults.set(fetchDate, forKey: "\(versionPrefix).ANONYMOUS_CONFIG.FETCH_DATE") cacheService.migrateLegacyCache() @@ -274,25 +275,25 @@ class DevCycleUserTest: XCTestCase { "Legacy anonymous fetch date should be removed") XCTAssertEqual( - defaults.object(forKey: "IDENTIFIED_CONFIG_\(identifiedUserId)") as? Data, + defaults.object(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(identifiedUserId)") as? Data, configData, "New identified config data should match original") XCTAssertNotNil( - defaults.object(forKey: "IDENTIFIED_CONFIG_\(identifiedUserId).EXPIRY_DATE"), + defaults.object(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(identifiedUserId).EXPIRY_DATE"), "New identified expiry date should be set") XCTAssertEqual( - defaults.object(forKey: "ANONYMOUS_CONFIG_\(anonymousUserId)") as? Data, + defaults.object(forKey: "\(versionPrefix).ANONYMOUS_CONFIG_\(anonymousUserId)") as? Data, configData, "New anonymous config data should match original") XCTAssertNotNil( - defaults.object(forKey: "ANONYMOUS_CONFIG_\(anonymousUserId).EXPIRY_DATE"), + defaults.object(forKey: "\(versionPrefix).ANONYMOUS_CONFIG_\(anonymousUserId).EXPIRY_DATE"), "New anonymous expiry date should be set") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(identifiedUserId)") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(identifiedUserId).EXPIRY_DATE") - defaults.removeObject(forKey: "ANONYMOUS_CONFIG_\(anonymousUserId)") - defaults.removeObject(forKey: "ANONYMOUS_CONFIG_\(anonymousUserId).EXPIRY_DATE") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(identifiedUserId)") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(identifiedUserId).EXPIRY_DATE") + defaults.removeObject(forKey: "\(versionPrefix).ANONYMOUS_CONFIG_\(anonymousUserId)") + defaults.removeObject(forKey: "\(versionPrefix).ANONYMOUS_CONFIG_\(anonymousUserId).EXPIRY_DATE") } func testLegacyCacheMigrationSkipsWhenNoData() { @@ -310,6 +311,7 @@ class DevCycleUserTest: XCTestCase { let defaults = UserDefaults.standard let userId = "test_user_123" + let versionPrefix = "VERSION_\(PlatformDetails().sdkVersion)" let legacyConfigData = "{\"variables\": {\"legacy\": \"oldValue\"}}".data(using: .utf8) let newConfigData = "{\"variables\": {\"new\": \"newValue\"}}".data(using: .utf8) let legacyFetchDate = Int(Date().timeIntervalSince1970) - 3600 // 1 hour ago @@ -319,9 +321,9 @@ class DevCycleUserTest: XCTestCase { defaults.set(userId, forKey: "IDENTIFIED_CONFIG.USER_ID") defaults.set(legacyFetchDate, forKey: "IDENTIFIED_CONFIG.FETCH_DATE") - defaults.set(newConfigData, forKey: "IDENTIFIED_CONFIG_\(userId)") - defaults.set(userId, forKey: "IDENTIFIED_CONFIG_\(userId).USER_ID") - defaults.set(newExpiryDate, forKey: "IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE") + defaults.set(newConfigData, forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId)") + defaults.set(userId, forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).USER_ID") + defaults.set(newExpiryDate, forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE") cacheService.migrateLegacyCache() @@ -336,17 +338,17 @@ class DevCycleUserTest: XCTestCase { "Legacy fetch date should be removed when new cache exists") XCTAssertEqual( - defaults.object(forKey: "IDENTIFIED_CONFIG_\(userId)") as? Data, + defaults.object(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId)") as? Data, newConfigData, "New config data should remain unchanged") XCTAssertEqual( - defaults.integer(forKey: "IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE"), + defaults.integer(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE"), newExpiryDate, "New expiry date should remain unchanged") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(userId)") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(userId).USER_ID") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId)") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).USER_ID") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE") } func testLegacyUserCacheCleanup() { @@ -390,6 +392,40 @@ class DevCycleUserTest: XCTestCase { defaults.object(forKey: "config"), "Legacy config cache should be removed after migration") } + + func testOtherSDKVersionConfigCacheCleanup() { + let cacheService = CacheService() + let defaults = UserDefaults.standard + + let userId = "test_user_123" + let versionPrefix = "VERSION_\(PlatformDetails().sdkVersion)" + let oldSdkCacheKey = "VERSION_1.23.0.IDENTIFIED_CONFIG_\(userId)" + let currentSdkCacheKey = "\(versionPrefix).IDENTIFIED_CONFIG_\(userId)" + + // Set up legacy config cache data + let legacyConfigData = "{\"variables\": {\"legacy\": \"oldValue\"}}".data(using: .utf8) + let newConfigData = "{\"variables\": {\"new\": \"newValue\"}}".data(using: .utf8) + + defaults.set(legacyConfigData, forKey: oldSdkCacheKey) + defaults.set(newConfigData, forKey: currentSdkCacheKey) + + // Verify legacy config cache exists + XCTAssertNotNil( + defaults.object(forKey: oldSdkCacheKey), "Legacy config cache should exist before migration") + + // Run migration + cacheService.migrateLegacyCache() + + // Verify legacy config cache is cleaned up + XCTAssertNil( + defaults.object(forKey: oldSdkCacheKey), + "Legacy config cache should be removed after migration") + + XCTAssertEqual( + defaults.object(forKey: currentSdkCacheKey) as? Data, + newConfigData, + "New config data should remain unchanged") + } } extension DevCycleUserTest {