diff --git a/Amplitude-Swift.xcodeproj/project.pbxproj b/Amplitude-Swift.xcodeproj/project.pbxproj index ce9b3297..affe464c 100644 --- a/Amplitude-Swift.xcodeproj/project.pbxproj +++ b/Amplitude-Swift.xcodeproj/project.pbxproj @@ -24,6 +24,8 @@ 8EDEC14255F82E24CEE00B36 /* AmplitudeSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */; }; 8EDEC2E0CC80DF79F5463ACC /* RemnantDataMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC6A9899998F823C278F7 /* RemnantDataMigrationTests.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 */; }; 8EDEC8F8DD2CDCD6568512F8 /* RemnantDataMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC19F9FBC98A0D4E5A513 /* RemnantDataMigration.swift */; }; 8EDECFCCF4219767F26210D6 /* Sessions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC2B8B38E04CDB51F0E83 /* Sessions.swift */; }; BA0359CA2A51585D007C383B /* legacy_v3.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = BA0359C92A51585D007C383B /* legacy_v3.sqlite */; }; @@ -111,6 +113,8 @@ 8EDEC19F9FBC98A0D4E5A513 /* RemnantDataMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemnantDataMigration.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 = ""; }; 8EDEC6A9899998F823C278F7 /* RemnantDataMigrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemnantDataMigrationTests.swift; sourceTree = ""; }; BA0359C92A51585D007C383B /* legacy_v3.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = legacy_v3.sqlite; sourceTree = ""; }; BA0639F52A4DD491000F1CEE /* LegacyDatabaseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDatabaseStorage.swift; sourceTree = ""; }; @@ -200,18 +204,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - BA0639F42A4DD46E000F1CEE /* Migration */ = { + 8EDEC33A32439724A363C433 /* Migration */ = { isa = PBXGroup; children = ( + 8EDECC2335BCF4C2EC3A6206 /* StoragePrefixMigration.swift */, BA0639F52A4DD491000F1CEE /* LegacyDatabaseStorage.swift */, 8EDEC19F9FBC98A0D4E5A513 /* RemnantDataMigration.swift */, ); path = Migration; sourceTree = ""; }; - BA994B972A4F484E00D0913F /* Migration */ = { + 8EDECBC5925DC68913C7CB89 /* Migration */ = { isa = PBXGroup; children = ( + 8EDEC9B98272069D70D08EA4 /* StoragePrefixMigrationTests.swift */, BA0359C92A51585D007C383B /* legacy_v3.sqlite */, BA994B9B2A4F4B7500D0913F /* legacy_v4.sqlite */, BA994B992A4F48DE00D0913F /* LegacyDatabaseStorageTests.swift */, @@ -307,7 +313,7 @@ path = Utilities; sourceTree = ""; }; - OBJ_5 = { + OBJ_5 /* */ = { isa = PBXGroup; children = ( OBJ_6 /* Package.swift */, @@ -320,12 +326,12 @@ OBJ_81 /* CONTRIBUTING.md */, OBJ_82 /* AmplitudeSwift.podspec */, ); + name = ""; sourceTree = ""; }; OBJ_53 /* Tests */ = { isa = PBXGroup; children = ( - BA994B972A4F484E00D0913F /* Migration */, OBJ_54 /* AmplitudeTests.swift */, OBJ_55 /* ConfigurationTests.swift */, OBJ_56 /* ConsoleLoggerTests.swift */, @@ -336,6 +342,7 @@ OBJ_69 /* TypesTests.swift */, OBJ_70 /* Utilities */, 8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */, + 8EDECBC5925DC68913C7CB89 /* Migration */, ); name = Tests; path = Tests/AmplitudeTests; @@ -373,7 +380,6 @@ OBJ_7 /* Sources */ = { isa = PBXGroup; children = ( - BA0639F42A4DD46E000F1CEE /* Migration */, OBJ_8 /* Amplitude.swift */, OBJ_9 /* Configuration.swift */, OBJ_10 /* ConsoleLogger.swift */, @@ -389,6 +395,7 @@ OBJ_42 /* Types.swift */, OBJ_43 /* Utilities */, 8EDEC2B8B38E04CDB51F0E83 /* Sessions.swift */, + 8EDEC33A32439724A363C433 /* Migration */, ); name = Sources; path = Sources/Amplitude; @@ -483,7 +490,7 @@ knownRegions = ( en, ); - mainGroup = OBJ_5; + mainGroup = OBJ_5 /* */; productRefGroup = OBJ_75 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -541,6 +548,7 @@ OBJ_158 /* UrlExtensionTests.swift in Sources */, 8EDEC4EE0DE1C89889F451B5 /* QueueTimeTests.swift in Sources */, 8EDEC14255F82E24CEE00B36 /* AmplitudeSessionTests.swift in Sources */, + 8EDEC972AEB33E4528F7FEEB /* StoragePrefixMigrationTests.swift in Sources */, BA994B9A2A4F48DE00D0913F /* LegacyDatabaseStorageTests.swift in Sources */, 8EDEC2E0CC80DF79F5463ACC /* RemnantDataMigrationTests.swift in Sources */, ); @@ -589,6 +597,7 @@ OBJ_122 /* QueueTimer.swift in Sources */, OBJ_124 /* UrlExtension.swift in Sources */, 8EDECFCCF4219767F26210D6 /* Sessions.swift in Sources */, + 8EDECD602E181B3E2E85D4DF /* StoragePrefixMigration.swift in Sources */, 8EDEC8F8DD2CDCD6568512F8 /* RemnantDataMigration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/Amplitude/Amplitude.swift b/Sources/Amplitude/Amplitude.swift index 95ce0e39..dcad439f 100644 --- a/Sources/Amplitude/Amplitude.swift +++ b/Sources/Amplitude/Amplitude.swift @@ -49,10 +49,19 @@ public class Amplitude { let contextPlugin = ContextPlugin() self.contextPlugin = contextPlugin + migrateApiKeyStorages() + if configuration.migrateLegacyData { RemnantDataMigration(self).execute() } + 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) @@ -302,4 +311,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 8da1db68..1147f791 100644 --- a/Sources/Amplitude/Configuration.swift +++ b/Sources/Amplitude/Configuration.swift @@ -66,9 +66,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/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, 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 4433c14c..a86031c9 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 dcddfee7..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, @@ -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,