diff --git a/OptimoveSDK/Sources/Classes/Components/Queue/OptistreamPersistentContainerConfigurator.swift b/OptimoveSDK/Sources/Classes/Components/Queue/OptistreamPersistentContainerConfigurator.swift new file mode 100644 index 00000000..89ba4b2d --- /dev/null +++ b/OptimoveSDK/Sources/Classes/Components/Queue/OptistreamPersistentContainerConfigurator.swift @@ -0,0 +1,25 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import CoreData +import Foundation + +final class OptistreamPersistentContainerConfigurator: PersistentContainerConfigurator { + enum Constants { + static let modelName = "OptistreamQueue" + static let folderName = "com.optimove.sdk.no-backup" + } + + let version: CoreDataMigrationVersion + + init(version: CoreDataMigrationVersion = .current) { + self.version = version + } + + let folderName: String? = Constants.folderName + let modelName: String = Constants.modelName + var managedObjectModel: ManagedObjectModel { + CoreDataModelDescription.makeOptistreamEventModel(version: version) + } + + var location: FileManagerLocation = .libraryDirectory +} diff --git a/OptimoveSDK/Sources/Classes/CoreData/PersistentContainer.swift b/OptimoveSDK/Sources/Classes/CoreData/PersistentContainer.swift index d503ce5f..582e369c 100644 --- a/OptimoveSDK/Sources/Classes/CoreData/PersistentContainer.swift +++ b/OptimoveSDK/Sources/Classes/CoreData/PersistentContainer.swift @@ -19,24 +19,22 @@ final class PersistentContainer: NSPersistentContainer { } } - private enum Constants { - static let modelName = "OptistreamQueue" - static let folderName = "com.optimove.sdk.no-backup" - } - private let migrator: CoreDataMigratorProtocol + private let persistentContainerConfigurator: PersistentContainerConfigurator private let storeType: PersistentStoreType init( - modelName: String = Constants.modelName, - version: CoreDataMigrationVersion = .current, + persistentContainerConfigurator: PersistentContainerConfigurator, migrator: CoreDataMigratorProtocol = CoreDataMigrator(), storeType: PersistentStoreType = .sql ) { - let mom = CoreDataModelDescription.makeOptistreamEventModel(version: version) self.migrator = migrator self.storeType = storeType - super.init(name: modelName, managedObjectModel: mom) + self.persistentContainerConfigurator = persistentContainerConfigurator + super.init( + name: persistentContainerConfigurator.modelName, + managedObjectModel: persistentContainerConfigurator.managedObjectModel + ) } func loadPersistentStores(storeName: String) throws { @@ -44,7 +42,8 @@ final class PersistentContainer: NSPersistentContainer { let persistentStoreDescription = NSPersistentStoreDescription() persistentStoreDescription.type = storeType.coreDataValue persistentStoreDescription.url = try FileManager.default.defineStoreURL( - folderName: Constants.folderName, + location: persistentContainerConfigurator.location, + folderName: persistentContainerConfigurator.folderName, storeName: storeName ) persistentStoreDescription.shouldMigrateStoreAutomatically = false @@ -90,14 +89,32 @@ final class PersistentContainer: NSPersistentContainer { } extension FileManager { - func defineStoreURL(folderName: String, storeName: String) throws -> URL { - let libraryDirectory = try unwrap(urls(for: .libraryDirectory, in: .userDomainMask).first) - let libraryStoreDirectoryURL = try unwrap(libraryDirectory.appendingPathComponent(folderName)) - let storeURL = try unwrap(libraryStoreDirectoryURL.appendingPathComponent("\(storeName).sqlite")) - guard !directoryExists(atUrl: libraryStoreDirectoryURL, isDirectory: true) else { + func defineLocation(_ location: FileManagerLocation) throws -> URL { + switch location { + case let .appGroupDirectory(url): + return url + case .libraryDirectory: + return try unwrap(urls(for: .libraryDirectory, in: .userDomainMask).first) + } + } + + func defineStoreURL( + location: FileManagerLocation, + folderName: String?, + storeName: String + ) throws -> URL { + let storeFolderURL = try { + let locationURL = try defineLocation(location) + if let folderName = folderName { + return try unwrap(locationURL.appendingPathComponent(folderName)) + } + return locationURL + }() + let storeURL = try unwrap(storeFolderURL.appendingPathComponent("\(storeName).sqlite")) + guard !directoryExists(atUrl: storeFolderURL, isDirectory: true) else { return try addSkipBackupAttributeToItemAtURL(url: storeURL) } - try createDirectory(at: libraryStoreDirectoryURL, withIntermediateDirectories: true) + try createDirectory(at: storeFolderURL, withIntermediateDirectories: true) return try addSkipBackupAttributeToItemAtURL(url: storeURL) } @@ -129,6 +146,10 @@ extension CoreDataModelDescription { } } + static func makeAnalyticsEventModel() -> NSManagedObjectModel { + return makeAnalyticsEventModelv1() + } + private static func makeOptistreamEventModelv1() -> NSManagedObjectModel { let modelDescription = CoreDataModelDescription( entities: [ @@ -170,4 +191,41 @@ extension CoreDataModelDescription { ) return modelDescription.makeModel() } + + private static func makeAnalyticsEventModelv1() -> NSManagedObjectModel { + let modelDescription = CoreDataModelDescription( + entities: [ + .entity( + name: KSEventModel.entityName, + managedObjectClass: KSEventModel.self, + attributes: [ + .attribute( + name: #keyPath(KSEventModel.eventType), + type: .stringAttributeType + ), + .attribute( + name: #keyPath(KSEventModel.happenedAt), + type: .integer64AttributeType, + defaultValue: 0 + ), + .attribute( + name: #keyPath(KSEventModel.properties), + type: .binaryDataAttributeType, + isOptional: true + ), + .attribute( + name: #keyPath(KSEventModel.uuid), + type: .stringAttributeType + ), + .attribute( + name: #keyPath(KSEventModel.userIdentifier), + type: .stringAttributeType, + isOptional: true + ), + ] + ), + ] + ) + return modelDescription.makeModel() + } } diff --git a/OptimoveSDK/Sources/Classes/CoreData/PersistentContainerConfigurator.swift b/OptimoveSDK/Sources/Classes/CoreData/PersistentContainerConfigurator.swift new file mode 100644 index 00000000..d28f5d00 --- /dev/null +++ b/OptimoveSDK/Sources/Classes/CoreData/PersistentContainerConfigurator.swift @@ -0,0 +1,19 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import CoreData +import Foundation +import OptimoveCore + +typealias ManagedObjectModel = NSManagedObjectModel + +enum FileManagerLocation { + case libraryDirectory + case appGroupDirectory(URL) +} + +protocol PersistentContainerConfigurator { + var folderName: String? { get } + var modelName: String { get } + var managedObjectModel: ManagedObjectModel { get } + var location: FileManagerLocation { get } +} diff --git a/OptimoveSDK/Sources/Classes/Extensions/NSManagedObjectContext+Utilities.swift b/OptimoveSDK/Sources/Classes/Extensions/NSManagedObjectContext+Utilities.swift index 80efeabc..5c51dbff 100644 --- a/OptimoveSDK/Sources/Classes/Extensions/NSManagedObjectContext+Utilities.swift +++ b/OptimoveSDK/Sources/Classes/Extensions/NSManagedObjectContext+Utilities.swift @@ -4,6 +4,17 @@ import CoreData import OptimoveCore extension NSManagedObjectContext { + enum Error: LocalizedError { + case unableToSaveEvent + + var errorDescription: String? { + switch self { + case .unableToSaveEvent: + return "Unable to save event" + } + } + } + /** Safe is determined by checking if the context has any persistent stores. - Returns: `False` if no persistent stores found. @@ -20,13 +31,44 @@ extension NSManagedObjectContext { - Returns: A value with generic type. */ func safeTryPerformAndWait(_ block: (Bool) throws -> T) throws -> T { - var result: Result? + var result: Result? performAndWait { result = Result { try block(isSafe) } } return try result!.get() } + // Async perform with throws error + func safeTryPerform(_ block: @escaping (NSManagedObjectContext) throws -> T, completion: @escaping (Result) -> Void) { + perform { + guard self.isSafe else { + completion(.failure(NSManagedObjectContext.Error.unableToSaveEvent)) + return + } + let result = Result { try block(self) } + completion(result) + } + } + + // Async/await perform with throws error + func safeTryPerform( + _ block: @escaping (NSManagedObjectContext) throws -> T) async throws -> T + { + guard isSafe else { + throw NSManagedObjectContext.Error.unableToSaveEvent + } + return try await withCheckedThrowingContinuation { continuation in + self.perform { + do { + let result = try block(self) + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } + /** Performs a synchronous block with the passed in boolean indicating if it's safe to perform operations. Safe is determined by checking if the context has any persistent stores. Throws an error if occurs. @@ -34,7 +76,7 @@ extension NSManagedObjectContext { - block: A block to perform. */ func safeTryPerformAndWait(_ block: (Bool) throws -> Void) throws { - var result: Result? + var result: Result? performAndWait { result = Result { try block(isSafe) } } diff --git a/OptimoveSDK/Sources/Classes/Factories/ComponentFactory.swift b/OptimoveSDK/Sources/Classes/Factories/ComponentFactory.swift index e6a4cdfb..3276cc71 100644 --- a/OptimoveSDK/Sources/Classes/Factories/ComponentFactory.swift +++ b/OptimoveSDK/Sources/Classes/Factories/ComponentFactory.swift @@ -13,7 +13,9 @@ final class ComponentFactory { { self.serviceLocator = serviceLocator self.coreEventFactory = coreEventFactory - persistentContainer = PersistentContainer() + persistentContainer = PersistentContainer( + persistentContainerConfigurator: OptistreamPersistentContainerConfigurator() + ) } func createRealtimeComponent(configuration: Configuration) throws -> RealTime { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticModel.swift b/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticModel.swift new file mode 100644 index 00000000..3396062b --- /dev/null +++ b/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticModel.swift @@ -0,0 +1,46 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import CoreData +import Foundation + +final class KSEventModel: NSManagedObject { + @discardableResult + static func insert( + into context: NSManagedObjectContext, + uuid: UUID = UUID(), + atTime: Date, + eventType: String, + userIdentifier: String, + properties: [String: Any]? = nil + ) throws -> KSEventModel { + let eventCD: KSEventModel = try context.insertObject() + eventCD.uuid = uuid.uuidString.lowercased() + eventCD.happenedAt = NSNumber(value: Int64(atTime.timeIntervalSince1970 * 1000)) + eventCD.eventType = eventType + eventCD.userIdentifier = userIdentifier + if let properties = properties { + eventCD.properties = try JSONSerialization.data(withJSONObject: properties) + } + return eventCD + } +} + +extension KSEventModel { + @NSManaged var uuid: String + @NSManaged var userIdentifier: String + @NSManaged var happenedAt: NSNumber + @NSManaged var eventType: String + @NSManaged var properties: Data? +} + +extension KSEventModel: Managed { + static var entityName: String { + return "Event" + } + + static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \KSEventModel.happenedAt, ascending: true)] + } +} + +extension KSEventModel {} diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticsHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticsHelper.swift new file mode 100644 index 00000000..be3af1b5 --- /dev/null +++ b/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticsHelper.swift @@ -0,0 +1,165 @@ +// Copyright © 2022 Optimove. All rights reserved. + +import CoreData +import Foundation +import OptimoveCore + +typealias SyncCompletedBlock = (Error?) -> Void + +final class AnalyticsHelper { + let eventsHttpClient: KSHttpClient + let optimobileHelper: OptimobileHelper + let container: PersistentContainer + let context: NSManagedObjectContext + var finishedInitializationToken: NSObjectProtocol? + + init( + httpClient: KSHttpClient, + optimobileHelper: OptimobileHelper, + container: PersistentContainer + ) throws { + self.container = container + try container.loadPersistentStores( + storeName: "KAnalyticsDbShared" + ) + context = container.newBackgroundContext() + context.mergePolicy = NSMergePolicy(merge: .mergeByPropertyStoreTrumpMergePolicyType) + + eventsHttpClient = httpClient + self.optimobileHelper = optimobileHelper + + finishedInitializationToken = NotificationCenter.default + .addObserver(forName: .optimobileInializationFinished, object: nil, queue: nil) { [weak self] notification in + DispatchQueue.global().async { + guard let self = self else { return } + self.flushEvents() + } + Logger.debug("Notification \(notification.name.rawValue) was processed") + } + } + + deinit { + eventsHttpClient.invalidateSessionCancellingTasks(false) + } + + func flushEvents() { + syncEvents() + } + + // MARK: Event Tracking + + func trackEvent(eventType: String, properties: [String: Any]?, immediateFlush: Bool) { + trackEvent( + eventType: eventType, + atTime: Date(), + properties: properties, + immediateFlush: immediateFlush + ) + } + + func trackEvent(eventType: String, atTime: Date, properties: [String: Any]?, immediateFlush: Bool, onSyncComplete: SyncCompletedBlock? = nil) { + if eventType == "" || (properties != nil && !JSONSerialization.isValidJSONObject(properties as Any)) { + Logger.error("Ignoring invalid event with empty type or non-serializable properties") + return + } + Task { + let currentUserIdentifier = optimobileHelper.currentUserIdentifier() + do { + try await context.safeTryPerform { context in + try KSEventModel.insert( + into: context, + atTime: atTime, + eventType: eventType, + userIdentifier: currentUserIdentifier + ) + try context.save() + } + + if immediateFlush { + syncEvents(onSyncComplete) + } + } catch { + Logger.error("Error saving event: \(error.localizedDescription)") + } + } + } + + private func syncEvents(_ onSyncComplete: SyncCompletedBlock? = nil) { + context.performAndWait { + // FIXME: Remove unnecessary performAndWait + let results = (try? fetchEventsBatch()) ?? [] + + if results.count == 0 { + onSyncComplete?(nil) + } else if results.count > 0 { + syncEventsBatch(events: results, onSyncComplete) + return + } + } + } + + private func syncEventsBatch( + events: [KSEventModel], + _ onSyncComplete: SyncCompletedBlock? = nil + ) { + var data = [] as [[String: Any?]] + var eventIds = [] as [NSManagedObjectID] + + for event in events { + var jsonProps = nil as Any? + if let props = event.properties { + jsonProps = try? JSONSerialization.jsonObject(with: props, options: JSONSerialization.ReadingOptions(rawValue: 0)) + } + + data.append([ + "type": event.eventType, + "uuid": event.uuid, + "timestamp": event.happenedAt, + "data": jsonProps, + "userId": event.userIdentifier, + ]) + eventIds.append(event.objectID) + } + + let path = "/v1/app-installs/\(optimobileHelper.installId())/events" + + eventsHttpClient.sendRequest(.POST, toPath: path, data: data, onSuccess: { _, _ in + if let err = self.pruneEventsBatch(eventIds) { + print("Failed to prune events batch: " + err.localizedDescription) + onSyncComplete?(err) + return + } + self.syncEvents(onSyncComplete) + }) { _, error, _ in + print("Failed to send events") + onSyncComplete?(error) + } + } + + private func pruneEventsBatch(_ eventIds: [NSManagedObjectID]) -> Error? { + var err: Error? + + context.performAndWait { + let request = NSBatchDeleteRequest(objectIDs: eventIds) + + do { + try context.execute(request) + } catch { + err = error + } + } + + return err + } + + private func fetchEventsBatch() throws -> [KSEventModel] { + return try context.safeTryPerformAndWait { _ in + try KSEventModel.fetch(in: context) { request in + request.fetchLimit = 100 + request.sortDescriptors = KSEventModel.defaultSortDescriptors + request.returnsObjectsAsFaults = false + request.includesPendingChanges = false + } + } + } +} diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticsPersistentContainerConfigurator.swift b/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticsPersistentContainerConfigurator.swift new file mode 100644 index 00000000..c96c3d08 --- /dev/null +++ b/OptimoveSDK/Sources/Classes/Optimobile/Analytics/AnalyticsPersistentContainerConfigurator.swift @@ -0,0 +1,20 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +final class AnalyticsPersistentContainerConfigurator: PersistentContainerConfigurator { + enum Constants { + static let modelName = "AnalyticsEvents" + } + + init() throws { + let url = try FileManager.optimoveAppGroupURL() + self.location = .appGroupDirectory(url) + } + + var folderName: String? = nil + let modelName: String = Constants.modelName + var managedObjectModel: ManagedObjectModel = + CoreDataModelDescription.makeAnalyticsEventModel() + var location: FileManagerLocation +} diff --git a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift deleted file mode 100644 index fe2d01a0..00000000 --- a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import CoreData -import Foundation -import OptimoveCore - -class KSEventModel: NSManagedObject { - @NSManaged var uuid: String - @NSManaged var userIdentifier: String - @NSManaged var happenedAt: NSNumber - @NSManaged var eventType: String - @NSManaged var properties: Data? -} - -typealias SyncCompletedBlock = (Error?) -> Void - -final class AnalyticsHelper { - let eventsHttpClient: KSHttpClient - let optimobileHelper: OptimobileHelper - private var analyticsContext: NSManagedObjectContext? - private var migrationAnalyticsContext: NSManagedObjectContext? - private var finishedInitializationToken: NSObjectProtocol? - - // MARK: Initialization - - init(httpClient: KSHttpClient, optimobileHelper: OptimobileHelper) { - analyticsContext = nil - migrationAnalyticsContext = nil - - eventsHttpClient = httpClient - self.optimobileHelper = optimobileHelper - - initContext() - - finishedInitializationToken = NotificationCenter.default - .addObserver(forName: .optimobileInializationFinished, object: nil, queue: nil) { [weak self] notification in - DispatchQueue.global().async { - guard let self = self else { return } - self.flushEvents() - } - Logger.debug("Notification \(notification.name.rawValue) was processed") - } - } - - deinit { - eventsHttpClient.invalidateSessionCancellingTasks(false) - } - - func flushEvents() { - if migrationAnalyticsContext != nil { - syncEvents(context: migrationAnalyticsContext) - } - syncEvents(context: analyticsContext) - } - - private func getMainStoreUrl(appGroupExists: Bool) -> URL? { - if !appGroupExists { - return getAppDbUrl() - } - - return getSharedDbUrl() - } - - private func getAppDbUrl() -> URL? { - let docsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last - let appDbUrl = URL(string: "KAnalyticsDb.sqlite", relativeTo: docsUrl) - - return appDbUrl - } - - private func getSharedDbUrl() -> URL? { - let sharedContainerPath = try? FileManager.optimoveAppGroupURL() - if sharedContainerPath == nil { - return nil - } - - return URL(string: "KAnalyticsDbShared.sqlite", relativeTo: sharedContainerPath) - } - - private func initContext() { - let appDbUrl = getAppDbUrl() - let appDbExists = appDbUrl == nil ? false : FileManager.default.fileExists(atPath: appDbUrl!.path) - let appGroupExists = true - - let storeUrl = getMainStoreUrl(appGroupExists: appGroupExists) - - if appGroupExists, appDbExists { - migrationAnalyticsContext = getManagedObjectContext(storeUrl: appDbUrl) - } - - analyticsContext = getManagedObjectContext(storeUrl: storeUrl) - } - - private func getManagedObjectContext(storeUrl: URL?) -> NSManagedObjectContext? { - let objectModel = getCoreDataModel() - let storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: objectModel) - let opts = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true] - - do { - try storeCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeUrl, options: opts) - } catch { - print("Failed to set up persistent store: " + error.localizedDescription) - return nil - } - - let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - context.performAndWait { - context.persistentStoreCoordinator = storeCoordinator - } - - return context - } - - // MARK: Event Tracking - - func trackEvent(eventType: String, properties: [String: Any]?, immediateFlush: Bool) { - trackEvent( - eventType: eventType, - atTime: Date(), - properties: properties, - immediateFlush: immediateFlush - ) - } - - func trackEvent(eventType: String, atTime: Date, properties: [String: Any]?, immediateFlush: Bool, onSyncComplete: SyncCompletedBlock? = nil) { - if eventType == "" || (properties != nil && !JSONSerialization.isValidJSONObject(properties as Any)) { - Logger.error("Ignoring invalid event with empty type or non-serializable properties") - return - } - - let currentUserIdentifier = optimobileHelper.currentUserIdentifier() - let work = { - guard let context = self.analyticsContext else { - print("No context, aborting") - return - } - - guard let entity = NSEntityDescription.entity(forEntityName: "Event", in: context) else { - print("Can't create entity, aborting") - return - } - - let event = KSEventModel(entity: entity, insertInto: nil) - - event.uuid = UUID().uuidString.lowercased() - event.happenedAt = NSNumber(value: Int64(atTime.timeIntervalSince1970 * 1000)) - event.eventType = eventType - event.userIdentifier = currentUserIdentifier - - if properties != nil { - let propsJson = try? JSONSerialization.data(withJSONObject: properties as Any, options: JSONSerialization.WritingOptions(rawValue: 0)) - - event.properties = propsJson - } - - context.insert(event) - do { - try context.save() - - if immediateFlush { - DispatchQueue.global().async { - self.syncEvents(context: self.analyticsContext, onSyncComplete) - } - } - } catch { - print("Failed to record event") - print(error) - } - } - - analyticsContext?.perform(work) - } - - private func syncEvents(context: NSManagedObjectContext?, _ onSyncComplete: SyncCompletedBlock? = nil) { - context?.performAndWait { - let results = fetchEventsBatch(context) - - if results.count == 0 { - onSyncComplete?(nil) - - if context === migrationAnalyticsContext { - removeAppDatabase() - } - } else if results.count > 0 { - syncEventsBatch(context, events: results, onSyncComplete) - return - } - } - } - - private func removeAppDatabase() { - if migrationAnalyticsContext == nil { - return - } - - guard let persStoreCoord = migrationAnalyticsContext!.persistentStoreCoordinator else { - return - } - - guard let store = persStoreCoord.persistentStores.last else { - return - } - - let storeUrl = persStoreCoord.url(for: store) - - migrationAnalyticsContext!.performAndWait { - migrationAnalyticsContext!.reset() - do { - try persStoreCoord.remove(store) - try FileManager.default.removeItem(at: storeUrl) - } catch {} - } - migrationAnalyticsContext = nil - } - - private func syncEventsBatch(_ context: NSManagedObjectContext?, events: [KSEventModel], _ onSyncComplete: SyncCompletedBlock? = nil) { - var data = [] as [[String: Any?]] - var eventIds = [] as [NSManagedObjectID] - - for event in events { - var jsonProps = nil as Any? - if let props = event.properties { - jsonProps = try? JSONSerialization.jsonObject(with: props, options: JSONSerialization.ReadingOptions(rawValue: 0)) - } - - data.append([ - "type": event.eventType, - "uuid": event.uuid, - "timestamp": event.happenedAt, - "data": jsonProps, - "userId": event.userIdentifier, - ]) - eventIds.append(event.objectID) - } - - let path = "/v1/app-installs/\(optimobileHelper.installId())/events" - - eventsHttpClient.sendRequest(.POST, toPath: path, data: data, onSuccess: { _, _ in - if let err = self.pruneEventsBatch(context, eventIds) { - print("Failed to prune events batch: " + err.localizedDescription) - onSyncComplete?(err) - return - } - self.syncEvents(context: context, onSyncComplete) - }) { _, error, _ in - print("Failed to send events") - onSyncComplete?(error) - } - } - - private func pruneEventsBatch(_ context: NSManagedObjectContext?, _ eventIds: [NSManagedObjectID]) -> Error? { - var err: Error? - - context?.performAndWait { - let request = NSBatchDeleteRequest(objectIDs: eventIds) - - do { - try context?.execute(request) - } catch { - err = error - } - } - - return err - } - - private func fetchEventsBatch(_ context: NSManagedObjectContext?) -> [KSEventModel] { - guard let context = context else { - return [] - } - - let request = NSFetchRequest(entityName: "Event") - request.returnsObjectsAsFaults = false - request.sortDescriptors = [NSSortDescriptor(key: "happenedAt", ascending: true)] - request.fetchLimit = 100 - request.includesPendingChanges = false - - do { - let results = try context.fetch(request) - return results - } catch { - print("Failed to fetch events batch: " + error.localizedDescription) - return [] - } - } - - // MARK: CoreData model definition - - private func getCoreDataModel() -> NSManagedObjectModel { - let model = NSManagedObjectModel() - - let eventEntity = NSEntityDescription() - eventEntity.name = "Event" - eventEntity.managedObjectClassName = NSStringFromClass(KSEventModel.self) - - var eventProps: [NSAttributeDescription] = [] - - let eventTypeProp = NSAttributeDescription() - eventTypeProp.name = "eventType" - eventTypeProp.attributeType = .stringAttributeType - eventTypeProp.isOptional = false - eventProps.append(eventTypeProp) - - let happenedAtProp = NSAttributeDescription() - happenedAtProp.name = "happenedAt" - happenedAtProp.attributeType = .integer64AttributeType - happenedAtProp.isOptional = false - happenedAtProp.defaultValue = 0 - eventProps.append(happenedAtProp) - - let propertiesProp = NSAttributeDescription() - propertiesProp.name = "properties" - propertiesProp.attributeType = .binaryDataAttributeType - propertiesProp.isOptional = true - eventProps.append(propertiesProp) - - let uuidProp = NSAttributeDescription() - uuidProp.name = "uuid" - uuidProp.attributeType = .stringAttributeType - uuidProp.isOptional = false - eventProps.append(uuidProp) - - let userIdProp = NSAttributeDescription() - userIdProp.name = "userIdentifier" - userIdProp.attributeType = .stringAttributeType - userIdProp.isOptional = true - eventProps.append(userIdProp) - - eventEntity.properties = eventProps - model.entities = [eventEntity] - - return model - } -} diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift index 367df907..62c389e0 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift @@ -113,7 +113,7 @@ final class Optimobile { try writeDefaultsKeys(config: config, storage: storage) - instance = Optimobile(config: config, storage: storage) + instance = try Optimobile(config: config, storage: storage) instance!.initializeHelpers() @@ -193,7 +193,7 @@ final class Optimobile { Optimobile.associateUserWithInstall(userIdentifier: initialUserId, storage: storage) } - private init(config: OptimobileConfig, storage: OptimoveStorage) { + private init(config: OptimobileConfig, storage: OptimoveStorage) throws { self.config = config let urlBuilder = UrlBuilder(storage: storage) networkFactory = NetworkFactory( @@ -206,9 +206,12 @@ final class Optimobile { optimobileHelper = OptimobileHelper( storage: storage ) - analyticsHelper = AnalyticsHelper( + analyticsHelper = try AnalyticsHelper( httpClient: networkFactory.build(for: .events), - optimobileHelper: optimobileHelper + optimobileHelper: optimobileHelper, + container: PersistentContainer( + persistentContainerConfigurator: AnalyticsPersistentContainerConfigurator() + ) ) sessionHelper = SessionHelper(sessionIdleTimeout: config.sessionIdleTimeout) diff --git a/OptimoveSDK/Tests/Sources/Components/OptiTrack/OptitrackTests.swift b/OptimoveSDK/Tests/Sources/Components/OptiTrack/OptitrackTests.swift index 7a646834..12fe4054 100644 --- a/OptimoveSDK/Tests/Sources/Components/OptiTrack/OptitrackTests.swift +++ b/OptimoveSDK/Tests/Sources/Components/OptiTrack/OptitrackTests.swift @@ -18,7 +18,9 @@ class OptitrackTests: OptimoveTestCase { networking = OptistreamNetworkingMock() let queue = try OptistreamQueueImpl( queueType: .track, - container: PersistentContainer(), + container: PersistentContainer( + persistentContainerConfigurator: OptistreamPersistentContainerConfigurator() + ), tenant: configuration.tenantID ) builder = OptistreamEventBuilder(