From 919903905f237d845fc7fd22266b0cb93bbbec4b Mon Sep 17 00:00:00 2001 From: Andrey Sokolov Date: Tue, 18 Jul 2023 22:46:07 +0400 Subject: [PATCH 1/2] fix: migrate 'api key' storage data to 'instance name' storage (#63) * fix: remove duplicated instanceName property * fix: migrate 'api key' storage data to `instance name` storage * chore: fix lint warnings * fix: rename eventFilesKey in preferences * fix: restore deviceId/userId from storage * fix: fixed userDefaults migration * chore: fix lint warning * chore: print() -> logger.warn() * chore: extract functions * fix: copy event files with duplicated file names --- Amplitude-Swift.xcodeproj/project.pbxproj | 24 +++ Sources/Amplitude/Amplitude.swift | 26 ++- Sources/Amplitude/Configuration.swift | 5 +- .../Migration/StoragePrefixMigration.swift | 108 ++++++++++++ Sources/Amplitude/Sessions.swift | 6 +- .../Storages/PersistentStorage.swift | 70 +++++--- Tests/AmplitudeTests/AmplitudeTests.swift | 20 ++- .../StoragePrefixMigrationTests.swift | 158 ++++++++++++++++++ .../Storages/PersistentStorageTests.swift | 6 +- .../Supports/TestUtilities.swift | 5 + ...ersistentStorageResponseHandlerTests.swift | 10 +- 11 files changed, 386 insertions(+), 52 deletions(-) create mode 100644 Sources/Amplitude/Migration/StoragePrefixMigration.swift create mode 100644 Tests/AmplitudeTests/Migration/StoragePrefixMigrationTests.swift diff --git a/Amplitude-Swift.xcodeproj/project.pbxproj b/Amplitude-Swift.xcodeproj/project.pbxproj index 3a687b15..0d4f6a51 100644 --- a/Amplitude-Swift.xcodeproj/project.pbxproj +++ b/Amplitude-Swift.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ /* Begin PBXBuildFile section */ 8EDEC14255F82E24CEE00B36 /* AmplitudeSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */; }; 8EDEC4EE0DE1C89889F451B5 /* QueueTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC4F83BFAA664749FAEF0 /* QueueTimeTests.swift */; }; + 8EDEC972AEB33E4528F7FEEB /* StoragePrefixMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC9B98272069D70D08EA4 /* StoragePrefixMigrationTests.swift */; }; + 8EDECD602E181B3E2E85D4DF /* StoragePrefixMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDECC2335BCF4C2EC3A6206 /* StoragePrefixMigration.swift */; }; 8EDECFCCF4219767F26210D6 /* Sessions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC2B8B38E04CDB51F0E83 /* Sessions.swift */; }; BA9BEA4B299FB43B00BC0F7C /* IdentifyInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */; }; BA9BEA4D299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */; }; @@ -104,6 +106,8 @@ 8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplitudeSessionTests.swift; sourceTree = ""; }; 8EDEC2B8B38E04CDB51F0E83 /* Sessions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sessions.swift; sourceTree = ""; }; 8EDEC4F83BFAA664749FAEF0 /* QueueTimeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueTimeTests.swift; sourceTree = ""; }; + 8EDEC9B98272069D70D08EA4 /* StoragePrefixMigrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoragePrefixMigrationTests.swift; sourceTree = ""; }; + 8EDECC2335BCF4C2EC3A6206 /* StoragePrefixMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoragePrefixMigration.swift; sourceTree = ""; }; BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyInterceptor.swift; sourceTree = ""; }; BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyInterceptorTests.swift; sourceTree = ""; }; OBJ_10 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; @@ -188,6 +192,22 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 8EDEC33A32439724A363C433 /* Migration */ = { + isa = PBXGroup; + children = ( + 8EDECC2335BCF4C2EC3A6206 /* StoragePrefixMigration.swift */, + ); + path = Migration; + sourceTree = ""; + }; + 8EDECBC5925DC68913C7CB89 /* Migration */ = { + isa = PBXGroup; + children = ( + 8EDEC9B98272069D70D08EA4 /* StoragePrefixMigrationTests.swift */, + ); + path = Migration; + sourceTree = ""; + }; OBJ_13 /* Events */ = { isa = PBXGroup; children = ( @@ -304,6 +324,7 @@ OBJ_69 /* TypesTests.swift */, OBJ_70 /* Utilities */, 8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */, + 8EDECBC5925DC68913C7CB89 /* Migration */, ); name = Tests; path = Tests/AmplitudeTests; @@ -356,6 +377,7 @@ OBJ_42 /* Types.swift */, OBJ_43 /* Utilities */, 8EDEC2B8B38E04CDB51F0E83 /* Sessions.swift */, + 8EDEC33A32439724A363C433 /* Migration */, ); name = Sources; path = Sources/Amplitude; @@ -495,6 +517,7 @@ OBJ_158 /* UrlExtensionTests.swift in Sources */, 8EDEC4EE0DE1C89889F451B5 /* QueueTimeTests.swift in Sources */, 8EDEC14255F82E24CEE00B36 /* AmplitudeSessionTests.swift in Sources */, + 8EDEC972AEB33E4528F7FEEB /* StoragePrefixMigrationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -540,6 +563,7 @@ OBJ_122 /* QueueTimer.swift in Sources */, OBJ_124 /* UrlExtension.swift in Sources */, 8EDECFCCF4219767F26210D6 /* Sessions.swift in Sources */, + 8EDECD602E181B3E2E85D4DF /* StoragePrefixMigration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Amplitude/Amplitude.swift b/Sources/Amplitude/Amplitude.swift index f31933de..50fce7e8 100644 --- a/Sources/Amplitude/Amplitude.swift +++ b/Sources/Amplitude/Amplitude.swift @@ -2,7 +2,6 @@ import Foundation public class Amplitude { public private(set) var configuration: Configuration - var instanceName: String private var inForeground = false var sessionId: Int64 { @@ -39,15 +38,22 @@ public class Amplitude { }() public init( - configuration: Configuration, - instanceName: String = Constants.Configuration.DEFAULT_INSTANCE + configuration: Configuration ) { self.configuration = configuration - self.instanceName = instanceName let contextPlugin = ContextPlugin() self.contextPlugin = contextPlugin + migrateApiKeyStorages() + + if let deviceId: String? = configuration.storageProvider.read(key: .DEVICE_ID) { + state.deviceId = deviceId + } + if let userId: String? = configuration.storageProvider.read(key: .USER_ID) { + state.userId = userId + } + // required plugin for specific platform, only has lifecyclePlugin now if let requiredPlugin = VendorSystem.current.requiredPlugin { _ = add(plugin: requiredPlugin) @@ -297,4 +303,16 @@ public class Amplitude { _ = self.flush() } } + + private func migrateApiKeyStorages() { + if let persistentStorage = configuration.storageProvider as? PersistentStorage { + let apiKeyStorage = PersistentStorage(storagePrefix: "\(PersistentStorage.DEFAULT_STORAGE_PREFIX)-\(configuration.apiKey)") + StoragePrefixMigration(source: apiKeyStorage, destination: persistentStorage, logger: logger).execute() + } + + if let persistentIdentifyStorage = configuration.identifyStorageProvider as? PersistentStorage { + let apiKeyIdentifyStorage = PersistentStorage(storagePrefix: "\(PersistentStorage.DEFAULT_STORAGE_PREFIX)-identify-\(configuration.apiKey)") + StoragePrefixMigration(source: apiKeyIdentifyStorage, destination: persistentIdentifyStorage, logger: logger).execute() + } + } } diff --git a/Sources/Amplitude/Configuration.swift b/Sources/Amplitude/Configuration.swift index dff5da67..73503822 100644 --- a/Sources/Amplitude/Configuration.swift +++ b/Sources/Amplitude/Configuration.swift @@ -64,9 +64,10 @@ public class Configuration { self.flushIntervalMillis = flushIntervalMillis self.instanceName = instanceName self.optOut = optOut - self.storageProvider = storageProvider ?? PersistentStorage(apiKey: apiKey) + self.storageProvider = storageProvider + ?? PersistentStorage(storagePrefix: "storage-\(instanceName)") self.identifyStorageProvider = identifyStorageProvider - ?? PersistentStorage(apiKey: apiKey, storagePrefix: "\(PersistentStorage.DEFAULT_STORAGE_PREFIX)-identify") + ?? PersistentStorage(storagePrefix: "identify-\(instanceName)") self.logLevel = logLevel self.loggerProvider = loggerProvider self.minIdLength = minIdLength diff --git a/Sources/Amplitude/Migration/StoragePrefixMigration.swift b/Sources/Amplitude/Migration/StoragePrefixMigration.swift new file mode 100644 index 00000000..aea27a54 --- /dev/null +++ b/Sources/Amplitude/Migration/StoragePrefixMigration.swift @@ -0,0 +1,108 @@ +import Foundation + +class StoragePrefixMigration { + let source: PersistentStorage + let destination: PersistentStorage + let logger: (any Logger)? + + init(source: PersistentStorage, destination: PersistentStorage, logger: (any Logger)?) { + self.source = source + self.destination = destination + self.logger = logger + } + + func execute() { + if source.storagePrefix == destination.storagePrefix { + return + } + + moveSourceEventFilesToDestination() + moveUserDefaults() + } + + private func moveUserDefaults() { + moveStringProperty(StorageKey.DEVICE_ID) + moveStringProperty(StorageKey.USER_ID) + moveIntegerProperty(StorageKey.PREVIOUS_SESSION_ID) + moveIntegerProperty(StorageKey.LAST_EVENT_TIME) + moveIntegerProperty(StorageKey.LAST_EVENT_ID) + moveEventsFileKey() + } + + private func moveSourceEventFilesToDestination() { + let sourceEventFiles = source.getEventFiles(includeUnfinished: true) + if sourceEventFiles.count == 0 { + return + } + // Ensure destination directory exists. + _ = destination.getEventsStorageDirectory(createDirectory: true) + + let fileManager = FileManager.default + for sourceEventFile in sourceEventFiles { + var destinationEventFile = sourceEventFile.path.replacingOccurrences(of: "/\(source.eventsFileKey)/", with: "/\(destination.eventsFileKey)/") + if fileManager.fileExists(atPath: destinationEventFile) { + var fileExtension = sourceEventFile.pathExtension + if fileExtension != "" { + fileExtension = ".\(fileExtension)" + } + destinationEventFile = "\((destinationEventFile as NSString).deletingPathExtension)-\(NSUUID().uuidString)\(fileExtension)" + } + do { + try fileManager.moveItem(atPath: sourceEventFile.path, toPath: destinationEventFile) + } catch { + logger?.warn(message: "Can't move \(sourceEventFile) to \(destinationEventFile): \(error)") + } + } + } + + private func moveStringProperty(_ key: StorageKey) { + guard let sourceValue: String = source.read(key: key) else { + return + } + + if destination.read(key: key) == nil { + do { + try destination.write(key: key, value: sourceValue) + } catch { + logger?.warn(message: "can't write destination \(key): \(error)") + } + } + + do { + try source.write(key: key, value: nil) + } catch { + logger?.warn(message: "can't write source \(key): \(error)") + } + } + + private func moveIntegerProperty(_ key: StorageKey) { + guard let sourceValue: Int = source.read(key: key) else { + return + } + + let destinationValue: Int? = destination.read(key: key) + if destinationValue == nil || destinationValue! < sourceValue { + do { + try destination.write(key: key, value: sourceValue) + } catch { + logger?.warn(message: "can't write destination \(key): \(error)") + } + } + + do { + try source.write(key: key, value: nil) + } catch { + logger?.warn(message: "can't clear source \(key): \(error)") + } + } + + private func moveEventsFileKey() { + if let sourceEventFileKey: Int = source.userDefaults?.integer(forKey: source.eventsFileKey) { + let destinationEventFileKey: Int? = destination.userDefaults?.integer(forKey: destination.eventsFileKey) + if destinationEventFileKey == nil || destinationEventFileKey! < sourceEventFileKey { + destination.userDefaults?.set(sourceEventFileKey, forKey: destination.eventsFileKey) + } + } + source.userDefaults?.removeObject(forKey: source.eventsFileKey) + } +} diff --git a/Sources/Amplitude/Sessions.swift b/Sources/Amplitude/Sessions.swift index a813aab3..bcf6b870 100644 --- a/Sources/Amplitude/Sessions.swift +++ b/Sources/Amplitude/Sessions.swift @@ -11,7 +11,7 @@ public class Sessions { do { try amplitude.storage.write(key: StorageKey.PREVIOUS_SESSION_ID, value: _sessionId) } catch { - print("Can't write PREVIOUS_SESSION_ID to storage: \(error)") + amplitude.logger?.warn(message: "Can't write PREVIOUS_SESSION_ID to storage: \(error)") } } } @@ -24,7 +24,7 @@ public class Sessions { do { try amplitude.storage.write(key: StorageKey.LAST_EVENT_ID, value: _lastEventId) } catch { - print("Can't write LAST_EVENT_ID to storage: \(error)") + amplitude.logger?.warn(message: "Can't write LAST_EVENT_ID to storage: \(error)") } } } @@ -37,7 +37,7 @@ public class Sessions { do { try amplitude.storage.write(key: StorageKey.LAST_EVENT_TIME, value: _lastEventTime) } catch { - print("Can't write LAST_EVENT_TIME to storage: \(error)") + amplitude.logger?.warn(message: "Can't write LAST_EVENT_TIME to storage: \(error)") } } } diff --git a/Sources/Amplitude/Storages/PersistentStorage.swift b/Sources/Amplitude/Storages/PersistentStorage.swift index 92b638b3..e91aece0 100644 --- a/Sources/Amplitude/Storages/PersistentStorage.swift +++ b/Sources/Amplitude/Storages/PersistentStorage.swift @@ -12,7 +12,7 @@ class PersistentStorage: Storage { let storagePrefix: String let userDefaults: UserDefaults? - let fileManager: FileManager? + let fileManager: FileManager private var outputStream: OutputFileStream? internal weak var amplitude: Amplitude? @@ -21,8 +21,10 @@ class PersistentStorage: Storage { let syncQueue = DispatchQueue(label: "syncPersistentStorage.amplitude.com") - init(apiKey: String, storagePrefix: String = PersistentStorage.DEFAULT_STORAGE_PREFIX) { - self.storagePrefix = "\(storagePrefix)-\(apiKey)" + init(storagePrefix: String) { + self.storagePrefix = storagePrefix == PersistentStorage.DEFAULT_STORAGE_PREFIX || storagePrefix.starts(with: "\(PersistentStorage.DEFAULT_STORAGE_PREFIX)-") + ? storagePrefix + : "\(PersistentStorage.DEFAULT_STORAGE_PREFIX)-\(storagePrefix)" self.userDefaults = UserDefaults(suiteName: "\(PersistentStorage.AMP_STORAGE_PREFIX).\(self.storagePrefix)") self.fileManager = FileManager.default self.eventCallbackMap = [String: EventCallback]() @@ -33,7 +35,7 @@ class PersistentStorage: Storage { switch key { case .EVENTS: if let event = value as? BaseEvent { - let eventStoreFile = getCurrentFile() + let eventStoreFile = getCurrentEventFile() self.storeEvent(toFile: eventStoreFile, event: event) if let eventCallback = event.callback, let eventInsertId = event.insertId { eventCallbackMap[eventInsertId] = eventCallback @@ -75,7 +77,7 @@ class PersistentStorage: Storage { func remove(eventBlock: EventBlock) { syncQueue.sync { do { - try fileManager!.removeItem(atPath: eventBlock.path) + try fileManager.removeItem(atPath: eventBlock.path) } catch { amplitude?.logger?.error(message: error.localizedDescription) } @@ -91,7 +93,7 @@ class PersistentStorage: Storage { storeEventsInNewFile(toFile: eventBlock.appendFileNameSuffix(suffix: "-1"), events: leftSplit) storeEventsInNewFile(toFile: eventBlock.appendFileNameSuffix(suffix: "-2"), events: rightSplit) do { - try fileManager!.removeItem(atPath: eventBlock.path) + try fileManager.removeItem(atPath: eventBlock.path) } catch { amplitude?.logger?.error(message: error.localizedDescription) } @@ -121,18 +123,21 @@ class PersistentStorage: Storage { userDefaults?.removeObject(forKey: key) } for url in urls { - try? fileManager!.removeItem(atPath: url.path) + try? fileManager.removeItem(atPath: url.path) } } } func rollover() { syncQueue.sync { - let currentFile = getCurrentFile() - if fileManager?.fileExists(atPath: currentFile.path) == false { + if getCurrentEventFileIndex() == nil { return } - if let attributes = try? fileManager?.attributesOfItem(atPath: currentFile.path), + let currentFile = getCurrentEventFile() + if fileManager.fileExists(atPath: currentFile.path) == false { + return + } + if let attributes = try? fileManager.attributesOfItem(atPath: currentFile.path), let fileSize = attributes[FileAttributeKey.size] as? UInt64, fileSize >= 0 { @@ -177,18 +182,22 @@ extension PersistentStorage { } extension PersistentStorage { - private var eventsFileKey: String { + internal var eventsFileKey: String { return "\(storagePrefix).\(StorageKey.EVENTS.rawValue).index" } - private func getCurrentFile() -> URL { + private func getCurrentEventFile() -> URL { var currentFileIndex = 0 - let index: Int = userDefaults?.integer(forKey: eventsFileKey) ?? 0 + let index: Int = getCurrentEventFileIndex() ?? 0 userDefaults?.set(index, forKey: eventsFileKey) currentFileIndex = index return getEventsFile(index: currentFileIndex) } + private func getCurrentEventFileIndex() -> Int? { + return userDefaults?.object(forKey: eventsFileKey) as? Int + } + private func getEventsFile(index: Int) -> URL { let dir = getEventsStorageDirectory() let fileURL = dir.appendingPathComponent("\(index)").appendingPathExtension( @@ -197,14 +206,19 @@ extension PersistentStorage { return fileURL } - private func getEventFiles(includeUnfinished: Bool = false) -> [URL] { + internal func getEventFiles(includeUnfinished: Bool = false) -> [URL] { var result = [URL]() + let eventsStorageDirectory = getEventsStorageDirectory(createDirectory: false) + if !fileManager.fileExists(atPath: eventsStorageDirectory.path) { + return result + } + // finish out any file in progress - let index = userDefaults?.integer(forKey: eventsFileKey) ?? 0 + let index = getCurrentEventFileIndex() ?? 0 finish(file: getEventsFile(index: index)) - let allFiles = try? fileManager!.contentsOfDirectory( + let allFiles = try? fileManager.contentsOfDirectory( at: getEventsStorageDirectory(), includingPropertiesForKeys: [], options: .skipsHiddenFiles @@ -224,7 +238,7 @@ extension PersistentStorage { return result } - private func getEventsStorageDirectory() -> URL { + internal func getEventsStorageDirectory(createDirectory: Bool = true) -> URL { // tvOS doesn't have access to document // macOS /Documents dir might be synced with iCloud #if os(tvOS) || os(macOS) @@ -233,12 +247,14 @@ extension PersistentStorage { let searchPathDirectory = FileManager.SearchPathDirectory.documentDirectory #endif - let urls = fileManager!.urls(for: searchPathDirectory, in: .userDomainMask) + let urls = fileManager.urls(for: searchPathDirectory, in: .userDomainMask) let docUrl = urls[0] let storageUrl = docUrl.appendingPathComponent("amplitude/\(eventsFileKey)/") - // try to create it, will fail if already exists. - // tvOS, watchOS regularly clear out data. - try? FileManager.default.createDirectory(at: storageUrl, withIntermediateDirectories: true, attributes: nil) + if createDirectory { + // try to create it, will fail if already exists. + // tvOS, watchOS regularly clear out data. + try? FileManager.default.createDirectory(at: storageUrl, withIntermediateDirectories: true, attributes: nil) + } return storageUrl } @@ -246,7 +262,7 @@ extension PersistentStorage { var storeFile = file var newFile = false - if fileManager?.fileExists(atPath: storeFile.path) == false { + if fileManager.fileExists(atPath: storeFile.path) == false { start(file: storeFile) newFile = true } else if outputStream == nil { @@ -255,13 +271,13 @@ extension PersistentStorage { } // Verify file size isn't too large - if let attributes = try? fileManager?.attributesOfItem(atPath: storeFile.path), + if let attributes = try? fileManager.attributesOfItem(atPath: storeFile.path), let fileSize = attributes[FileAttributeKey.size] as? UInt64, fileSize >= PersistentStorage.MAX_FILE_SIZE { finish(file: storeFile) // Set the new file path - storeFile = getCurrentFile() + storeFile = getCurrentEventFile() start(file: storeFile) newFile = true } @@ -284,7 +300,7 @@ extension PersistentStorage { private func storeEventsInNewFile(toFile file: URL, events: [BaseEvent]) { let storeFile = file - guard fileManager?.fileExists(atPath: storeFile.path) != true else { + guard fileManager.fileExists(atPath: storeFile.path) != true else { return } @@ -342,12 +358,12 @@ extension PersistentStorage { let fileWithoutTemp = file.deletingPathExtension() do { - try fileManager?.moveItem(at: file, to: fileWithoutTemp) + try fileManager.moveItem(at: file, to: fileWithoutTemp) } catch { amplitude?.logger?.error(message: "Unable to rename file: \(file.path)") } - let currentFileIndex: Int = (userDefaults?.integer(forKey: eventsFileKey) ?? 0) + 1 + let currentFileIndex: Int = (getCurrentEventFileIndex() ?? 0) + 1 userDefaults?.set(currentFileIndex, forKey: eventsFileKey) } } diff --git a/Tests/AmplitudeTests/AmplitudeTests.swift b/Tests/AmplitudeTests/AmplitudeTests.swift index a5e0ac55..bcd1cd65 100644 --- a/Tests/AmplitudeTests/AmplitudeTests.swift +++ b/Tests/AmplitudeTests/AmplitudeTests.swift @@ -22,8 +22,8 @@ final class AmplitudeTests: XCTestCase { configuration = Configuration(apiKey: apiKey) - storage = FakePersistentStorage(apiKey: apiKey) - interceptStorage = FakePersistentStorage(apiKey: apiKey) + storage = FakePersistentStorage(storagePrefix: "storage") + interceptStorage = FakePersistentStorage(storagePrefix: "intercept") configurationWithFakeStorage = Configuration( apiKey: apiKey, storageProvider: storage, @@ -42,7 +42,7 @@ final class AmplitudeTests: XCTestCase { func testInit() { XCTAssertEqual( - Amplitude(configuration: configuration).instanceName, + Amplitude(configuration: configuration).configuration.instanceName, Constants.Configuration.DEFAULT_INSTANCE ) } @@ -51,7 +51,11 @@ final class AmplitudeTests: XCTestCase { let amplitude = Amplitude(configuration: configurationWithFakeStorage) XCTAssertEqual(amplitude.getDeviceId() != nil, true) let deviceIdUuid = amplitude.getDeviceId()! - XCTAssertEqual(storage.haveBeenCalledWith, ["write(key: \(StorageKey.DEVICE_ID.rawValue), \(deviceIdUuid))"]) + XCTAssertEqual(storage.haveBeenCalledWith, [ + "read(key: device_id)", + "read(key: user_id)", + "write(key: \(StorageKey.DEVICE_ID.rawValue), \(deviceIdUuid))" + ]) } func testContext() { @@ -115,7 +119,7 @@ final class AmplitudeTests: XCTestCase { amplitude.setUserId(userId: "test-user") XCTAssertEqual(amplitude.getUserId(), "test-user") - XCTAssertEqual(storage.haveBeenCalledWith[1], "write(key: \(StorageKey.USER_ID.rawValue), test-user)") + XCTAssertEqual(storage.haveBeenCalledWith[3], "write(key: \(StorageKey.USER_ID.rawValue), test-user)") } func testSetDeviceId() { @@ -125,7 +129,7 @@ final class AmplitudeTests: XCTestCase { amplitude.setDeviceId(deviceId: "test-device") XCTAssertEqual(amplitude.getDeviceId(), "test-device") - XCTAssertEqual(storage.haveBeenCalledWith[1], "write(key: \(StorageKey.DEVICE_ID.rawValue), test-device)") + XCTAssertEqual(storage.haveBeenCalledWith[3], "write(key: \(StorageKey.DEVICE_ID.rawValue), test-device)") } func testInterceptedIdentifyIsSentOnFlush() { @@ -150,8 +154,8 @@ final class AmplitudeTests: XCTestCase { func testInterceptedIdentifyWithPersistentStorage() { let apiKey = "testApiKeyPersist" - storageTest = TestPersistentStorage(apiKey: apiKey) - interceptStorageTest = TestPersistentStorage(apiKey: apiKey, storagePrefix: "identify") + storageTest = TestPersistentStorage(storagePrefix: "storage") + interceptStorageTest = TestPersistentStorage(storagePrefix: "identify") let amplitude = Amplitude(configuration: Configuration( apiKey: apiKey, storageProvider: storageTest, diff --git a/Tests/AmplitudeTests/Migration/StoragePrefixMigrationTests.swift b/Tests/AmplitudeTests/Migration/StoragePrefixMigrationTests.swift new file mode 100644 index 00000000..83e7bb67 --- /dev/null +++ b/Tests/AmplitudeTests/Migration/StoragePrefixMigrationTests.swift @@ -0,0 +1,158 @@ +import XCTest + +@testable import Amplitude_Swift + +final class StoragePrefixMigrationTests: XCTestCase { + func testUserDefaults() throws { + let source = PersistentStorage(storagePrefix: NSUUID().uuidString) + let destination = PersistentStorage(storagePrefix: NSUUID().uuidString) + + try source.write(key: StorageKey.DEVICE_ID, value: "source-device") + try source.write(key: StorageKey.USER_ID, value: "source-user") + try source.write(key: StorageKey.PREVIOUS_SESSION_ID, value: 123) + try source.write(key: StorageKey.LAST_EVENT_TIME, value: 456) + try source.write(key: StorageKey.LAST_EVENT_ID, value: 789) + source.userDefaults?.set(12345, forKey: source.eventsFileKey) + + var destinationDeviceId: String? = destination.read(key: StorageKey.DEVICE_ID) + var destinationUserId: String? = destination.read(key: StorageKey.DEVICE_ID) + var destinationPreviousSessionId: Int? = destination.read(key: StorageKey.PREVIOUS_SESSION_ID) + var destinationLastEventTime: Int? = destination.read(key: StorageKey.LAST_EVENT_TIME) + var destinationLastEventId: Int? = destination.read(key: StorageKey.LAST_EVENT_ID) + var destinationEventsFileKey = destination.userDefaults?.object(forKey: destination.eventsFileKey) + XCTAssertNil(destinationDeviceId) + XCTAssertNil(destinationUserId) + XCTAssertNil(destinationPreviousSessionId) + XCTAssertNil(destinationLastEventTime) + XCTAssertNil(destinationLastEventId) + XCTAssertNil(destinationEventsFileKey) + + let migration = StoragePrefixMigration(source: source, destination: destination, logger: ConsoleLogger()) + migration.execute() + + let sourceDeviceId: String? = source.read(key: StorageKey.DEVICE_ID) + let sourceUserId: String? = source.read(key: StorageKey.USER_ID) + let sourcePreviousSessionId: Int? = source.read(key: StorageKey.PREVIOUS_SESSION_ID) + let sourceLastEventTime: Int? = source.read(key: StorageKey.LAST_EVENT_TIME) + let sourceLastEventId: Int? = source.read(key: StorageKey.LAST_EVENT_ID) + let sourceEventsFileKey = source.userDefaults?.object(forKey: source.eventsFileKey) + XCTAssertNil(sourceDeviceId) + XCTAssertNil(sourceUserId) + XCTAssertNil(sourcePreviousSessionId) + XCTAssertNil(sourceLastEventTime) + XCTAssertNil(sourceLastEventId) + XCTAssertNil(sourceEventsFileKey) + + destinationDeviceId = destination.read(key: StorageKey.DEVICE_ID) + destinationUserId = destination.read(key: StorageKey.USER_ID) + destinationPreviousSessionId = destination.read(key: StorageKey.PREVIOUS_SESSION_ID) + destinationLastEventTime = destination.read(key: StorageKey.LAST_EVENT_TIME) + destinationLastEventId = destination.read(key: StorageKey.LAST_EVENT_ID) + destinationEventsFileKey = destination.userDefaults?.object(forKey: destination.eventsFileKey) + XCTAssertEqual(destinationDeviceId, "source-device") + XCTAssertEqual(destinationUserId, "source-user") + XCTAssertEqual(destinationPreviousSessionId, 123) + XCTAssertEqual(destinationLastEventTime, 456) + XCTAssertEqual(destinationLastEventId, 789) + XCTAssertEqual(destinationEventsFileKey as? Int, 12345) + } + + func testEventFiles() throws { + let source = PersistentStorage(storagePrefix: NSUUID().uuidString) + let destination = PersistentStorage(storagePrefix: NSUUID().uuidString) + + try source.write(key: StorageKey.EVENTS, value: BaseEvent(eventType: "event-1")) + try source.write(key: StorageKey.EVENTS, value: BaseEvent(eventType: "event-2")) + source.rollover() + try source.write(key: StorageKey.EVENTS, value: BaseEvent(eventType: "event-3")) + source.rollover() + try source.write(key: StorageKey.EVENTS, value: BaseEvent(eventType: "event-4")) + + var sourceEventFiles = source.getEventFiles(includeUnfinished: true) + XCTAssertEqual(sourceEventFiles.count, 3) + + let sourceFileSizes = try sourceEventFiles.map{ try getFileSize($0) } + + var destinationEventFiles = destination.getEventFiles(includeUnfinished: true) + XCTAssertEqual(destinationEventFiles.count, 0) + + let migration = StoragePrefixMigration(source: source, destination: destination, logger: ConsoleLogger()) + migration.execute() + + sourceEventFiles = source.getEventFiles(includeUnfinished: true) + XCTAssertEqual(sourceEventFiles.count, 0) + + destinationEventFiles = destination.getEventFiles(includeUnfinished: true) + XCTAssertEqual(destinationEventFiles.count, 3) + + for (index, destinationEventFile) in destinationEventFiles.enumerated() { + let fileSize = try getFileSize(destinationEventFile) + XCTAssertEqual(fileSize, sourceFileSizes[index]) + } + } + + func testMissingSource() throws { + let source = PersistentStorage(storagePrefix: NSUUID().uuidString) + let destination = PersistentStorage(storagePrefix: NSUUID().uuidString) + + var destinationDeviceId: String? = destination.read(key: StorageKey.DEVICE_ID) + var destinationLastEventId: Int? = destination.read(key: StorageKey.LAST_EVENT_ID) + XCTAssertNil(destinationDeviceId) + XCTAssertNil(destinationLastEventId) + + let migration = StoragePrefixMigration(source: source, destination: destination, logger: ConsoleLogger()) + migration.execute() + + destinationDeviceId = destination.read(key: StorageKey.DEVICE_ID) + destinationLastEventId = destination.read(key: StorageKey.LAST_EVENT_ID) + XCTAssertNil(destinationDeviceId) + XCTAssertNil(destinationLastEventId) + + let destinationEventFiles = destination.getEventFiles(includeUnfinished: true) + XCTAssertEqual(destinationEventFiles.count, 0) + + let sourceEventsStorageDirectory = source.getEventsStorageDirectory(createDirectory: false) + XCTAssertFalse(FileManager.default.fileExists(atPath: sourceEventsStorageDirectory.path)) + } + + func testMoveEventFilesWithDuplicatedName() throws { + let source = PersistentStorage(storagePrefix: NSUUID().uuidString) + let destination = PersistentStorage(storagePrefix: NSUUID().uuidString) + + try source.write(key: StorageKey.EVENTS, value: BaseEvent(eventType: "event-1")) + source.rollover() + try source.write(key: StorageKey.EVENTS, value: BaseEvent(eventType: "event-11")) + + var sourceEventFiles = source.getEventFiles(includeUnfinished: true) + XCTAssertEqual(sourceEventFiles.count, 2) + + let sourceFileSizes = try sourceEventFiles.map{ try getFileSize($0) } + + try destination.write(key: StorageKey.EVENTS, value: BaseEvent(eventType: "event-ABC")) + destination.rollover() + + var destinationEventFiles = destination.getEventFiles(includeUnfinished: true) + XCTAssertEqual(destinationEventFiles.count, 1) + + let destinationFileSizes = try destinationEventFiles.map{ try getFileSize($0) } + + let migration = StoragePrefixMigration(source: source, destination: destination, logger: ConsoleLogger()) + migration.execute() + + sourceEventFiles = source.getEventFiles(includeUnfinished: true) + XCTAssertEqual(sourceEventFiles.count, 0) + + destinationEventFiles = destination.getEventFiles(includeUnfinished: true) + XCTAssertEqual(destinationEventFiles[0].lastPathComponent, "1") + XCTAssertEqual(destinationEventFiles[1].lastPathComponent.prefix(2), "0-") + XCTAssertEqual(destinationEventFiles[2].lastPathComponent, "0") + XCTAssertEqual(try getFileSize(destinationEventFiles[0]), sourceFileSizes[0]) + XCTAssertEqual(try getFileSize(destinationEventFiles[1]), sourceFileSizes[1]) + XCTAssertEqual(try getFileSize(destinationEventFiles[2]), destinationFileSizes[0]) + } + + private func getFileSize(_ url: URL) throws -> Int { + let resources = try url.resourceValues(forKeys: [.fileSizeKey]) + return resources.fileSize! + } +} diff --git a/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift b/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift index a5396e4a..11dda8cf 100644 --- a/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift +++ b/Tests/AmplitudeTests/Storages/PersistentStorageTests.swift @@ -11,7 +11,7 @@ import XCTest final class PersistentStorageTests: XCTestCase { func testIsBasicType() { - let persistentStorage = PersistentStorage(apiKey: "") + let persistentStorage = PersistentStorage(storagePrefix: "storage") var isValueBasicType = persistentStorage.isBasicType(value: 111) XCTAssertEqual(isValueBasicType, true) @@ -35,7 +35,7 @@ final class PersistentStorageTests: XCTestCase { } func testWrite() { - let persistentStorage = PersistentStorage(apiKey: "xxx-api-key") + let persistentStorage = PersistentStorage(storagePrefix: "xxx-instance") try? persistentStorage.write( key: StorageKey.EVENTS, value: BaseEvent(eventType: "test1") @@ -45,7 +45,7 @@ final class PersistentStorageTests: XCTestCase { value: BaseEvent(eventType: "test2") ) let eventFiles: [URL]? = persistentStorage.read(key: StorageKey.EVENTS) - XCTAssertEqual(eventFiles?[0].absoluteString.contains("xxx-api-key.events.index"), true) + XCTAssertEqual(eventFiles?[0].absoluteString.contains("xxx-instance.events.index"), true) XCTAssertNotEqual(eventFiles?[0].pathExtension, PersistentStorage.TEMP_FILE_EXTENSION) persistentStorage.reset() } diff --git a/Tests/AmplitudeTests/Supports/TestUtilities.swift b/Tests/AmplitudeTests/Supports/TestUtilities.swift index a377d832..b26ea79e 100644 --- a/Tests/AmplitudeTests/Supports/TestUtilities.swift +++ b/Tests/AmplitudeTests/Supports/TestUtilities.swift @@ -224,6 +224,11 @@ class FakePersistentStorage: PersistentStorage { override func write(key: StorageKey, value: Any?) throws { haveBeenCalledWith.append("write(key: \(key.rawValue), \(String(describing: value!)))") } + + override func read(key: StorageKey) -> T? { + haveBeenCalledWith.append("read(key: \(key.rawValue))") + return nil + } } class TestPersistentStorage: PersistentStorage { diff --git a/Tests/AmplitudeTests/Utilities/PersistentStorageResponseHandlerTests.swift b/Tests/AmplitudeTests/Utilities/PersistentStorageResponseHandlerTests.swift index 78868bc9..79cf32f0 100644 --- a/Tests/AmplitudeTests/Utilities/PersistentStorageResponseHandlerTests.swift +++ b/Tests/AmplitudeTests/Utilities/PersistentStorageResponseHandlerTests.swift @@ -19,9 +19,9 @@ final class PersistentStorageResponseHandlerTests: XCTestCase { override func setUp() { super.setUp() - configuration = Configuration(apiKey: "testApiKey") + storage = PersistentStorage(storagePrefix: "storage") + configuration = Configuration(apiKey: "testApiKey", storageProvider: storage) amplitude = Amplitude(configuration: configuration) - storage = PersistentStorage(apiKey: "testApiKey") eventPipeline = EventPipeline(amplitude: amplitude) eventBlock = URL(string: "test") } @@ -50,7 +50,7 @@ final class PersistentStorageResponseHandlerTests: XCTestCase { {"event_type":"test","insert_id":"c8d58999-7539-4184-8a7d-54302697baf0","user_id":"test-user"} ] """ - let fakePersistentStorage = FakePersistentStorage(apiKey: "testApiKey") + let fakePersistentStorage = FakePersistentStorage(storagePrefix: "storage") let handler = PersistentStorageResponseHandler( configuration: configuration, storage: fakePersistentStorage, @@ -83,7 +83,7 @@ final class PersistentStorageResponseHandlerTests: XCTestCase { ] """ - let fakePersistentStorage = FakePersistentStorage(apiKey: "testApiKey") + let fakePersistentStorage = FakePersistentStorage(storagePrefix: "storage") let handler = PersistentStorageResponseHandler( configuration: configuration, storage: fakePersistentStorage, @@ -108,7 +108,7 @@ final class PersistentStorageResponseHandlerTests: XCTestCase { ] """ - let fakePersistentStorage = FakePersistentStorage(apiKey: "testApiKey") + let fakePersistentStorage = FakePersistentStorage(storagePrefix: "storage") let handler = PersistentStorageResponseHandler( configuration: configuration, storage: fakePersistentStorage, From 74992cee7c10d8bde20aaf1a17f5f02611359d4e Mon Sep 17 00:00:00 2001 From: Justin Fiedler Date: Tue, 18 Jul 2023 12:29:59 -0700 Subject: [PATCH 2/2] fix: made EventOptions.init public (#64) --- Sources/Amplitude/Events/EventOptions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Amplitude/Events/EventOptions.swift b/Sources/Amplitude/Events/EventOptions.swift index 4e2b1008..745b58fc 100644 --- a/Sources/Amplitude/Events/EventOptions.swift +++ b/Sources/Amplitude/Events/EventOptions.swift @@ -47,7 +47,7 @@ public class EventOptions { public var partnerId: String? internal var attempts: Int - init( + public init( userId: String? = nil, deviceId: String? = nil, timestamp: Int64? = nil,