From 4f06b26cc7fd7032668a0990f759f05850053b19 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Mon, 16 Dec 2024 15:56:13 -0300 Subject: [PATCH 01/17] CoreData PoC with `EmbraceUploadCache` --- .../Cache/EmbraceUploadCache.swift | 291 +++++++++--------- .../Cache/UploadDataRecord.swift | 85 ++--- .../EmbraceUploadInternal/EmbraceUpload.swift | 30 +- .../Operations/EmbraceUploadOperation.swift | 2 +- .../Options/EmbraceUpload+CacheOptions.swift | 12 +- ...mbraceUploadCacheTests+ClearDataDate.swift | 87 +++--- .../EmbraceUploadCacheTests.swift | 201 ++++++------ .../EmbraceUploadTests.swift | 64 ++-- Tests/TestSupport/CoreDataListener.swift | 41 +++ 9 files changed, 425 insertions(+), 388 deletions(-) create mode 100644 Tests/TestSupport/CoreDataListener.swift diff --git a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift index 0da44012..d86abcfa 100644 --- a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift +++ b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift @@ -5,28 +5,58 @@ import Foundation import EmbraceOTelInternal import EmbraceCommonInternal -import GRDB +import CoreData /// Class that handles all the cached upload data generated by the Embrace SDK. class EmbraceUploadCache { private(set) var options: EmbraceUpload.CacheOptions - private(set) var dbQueue: DatabaseQueue + let container: NSPersistentContainer + let context: NSManagedObjectContext let logger: InternalLogger init(options: EmbraceUpload.CacheOptions, logger: InternalLogger) throws { self.options = options self.logger = logger - // create sqlite file - dbQueue = try Self.createDBQueue(options: options, logger: logger) + // create core data stack + let model = NSManagedObjectModel() + model.entities = [UploadDataRecord.entityDescription] - // define tables - try dbQueue.write { db in - try UploadDataRecord.defineTable(db: db) + self.container = NSPersistentContainer(name: "EmbraceUploadCache", managedObjectModel: model) + + switch options.storageMechanism { + case .inMemory: + let description = NSPersistentStoreDescription() + description.type = NSInMemoryStoreType + self.container.persistentStoreDescriptions = [description] + + case let .onDisk(baseURL, _): + try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) + let description = NSPersistentStoreDescription() + description.url = options.fileURL + self.container.persistentStoreDescriptions = [description] + } + + container.loadPersistentStores { _, error in + if let error { + logger.error("Error initializing EmbraceUpload cache!: \(error.localizedDescription)") + } } - try clearStaleDataIfNeeded() + self.context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + self.context.persistentStoreCoordinator = self.container.persistentStoreCoordinator + } + + // Saves all changes on the current context to disk + func save() { + context.perform { [weak self] in + do { + try self?.context.save() + } catch { + self?.logger.warning("Erro saving EmbraceUploadCache: \(error.localizedDescription)") + } + } } /// Fetches the cached upload data for the given identifier. @@ -34,26 +64,43 @@ class EmbraceUploadCache { /// - id: Identifier of the data /// - type: Type of the data /// - Returns: The cached `UploadDataRecord`, if any - public func fetchUploadData(id: String, type: EmbraceUploadType) throws -> UploadDataRecord? { - try dbQueue.read { db in - return try UploadDataRecord.fetchOne(db, key: ["id": id, "type": type.rawValue]) + public func fetchUploadData(id: String, type: EmbraceUploadType) -> UploadDataRecord? { + + var result: UploadDataRecord? + context.performAndWait { + do { + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + request.predicate = NSPredicate(format: "id == %@ AND type == %i", id, type.rawValue) + + result = try context.fetch(request).first + } catch { } } + return result } /// Fetches all the cached upload data. /// - Returns: An array containing all the cached `UploadDataRecords` public func fetchAllUploadData() throws -> [UploadDataRecord] { - try dbQueue.read { db in - return try UploadDataRecord - .order(Column("date").asc) - .fetchAll(db) + + var result: [UploadDataRecord] = [] + context.performAndWait { + do { + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + result = try context.fetch(request) + } catch { } } + return result } /// Removes stale data based on size or date, if they're limited in options. @discardableResult public func clearStaleDataIfNeeded() throws -> UInt { - let limitDays = options.cacheDaysLimit - let recordsToDelete = limitDays > 0 ? try fetchRecordsToDeleteBasedOnDate(maxDays: limitDays) : [] + guard options.cacheDaysLimit > 0 else { + return 0 + } + + let now = Date().timeIntervalSince1970 + let lastValidTime = now - TimeInterval(options.cacheDaysLimit * 86400) // (60 * 60 * 24) = 86400 seconds per day + let recordsToDelete = fetchRecordsToDelete(dateLimit: Date(timeIntervalSince1970: lastValidTime)) let deleteCount = recordsToDelete.count if deleteCount > 0 { @@ -63,9 +110,9 @@ class EmbraceUploadCache { attributes: ["removed": "\(deleteCount)"]) .markAsPrivate() span.setStartTime(time: Date()) + let startedSpan = span.startSpan() - try deleteRecords(recordIDs: recordsToDelete) - try dbQueue.vacuum() + deleteRecords(recordsToDelete) startedSpan.end() return UInt(deleteCount) @@ -79,42 +126,74 @@ class EmbraceUploadCache { /// - id: Identifier of the data /// - type: Type of the data /// - data: Data to cache - /// - Returns: The newly cached `UploadDataRecord` - @discardableResult func saveUploadData(id: String, type: EmbraceUploadType, data: Data) throws -> UploadDataRecord { - let record = UploadDataRecord(id: id, type: type.rawValue, data: data, attemptCount: 0, date: Date()) - try saveUploadData(record) + /// - Returns: Boolean indicating if the operation was successful + @discardableResult func saveUploadData(id: String, type: EmbraceUploadType, data: Data) -> Bool { + + // update if it already exists + if let record = fetchUploadData(id: id, type: type) { + context.perform { [weak self] in + record.data = data + self?.save() + } + + return true + } + + // check limit and delete if necessary + checkCountLimit() + + // insert new + var result = true + + context.performAndWait { + let record = UploadDataRecord.create( + context: context, + id: id, + type: type.rawValue, + data: data, + attemptCount: 0, + date: Date() + ) + + do { + try context.save() + } catch { + context.delete(record) + result = false + } + } - return record + return result } - /// Saves the given `UploadDataRecord` to the cache. - /// - Parameter record: `UploadDataRecord` instance to save - func saveUploadData(_ record: UploadDataRecord) throws { - try dbQueue.write { [weak self] db in + // Checks the amount of records stored and deletes the oldest ones if the total amount + // surpasses the limit. + func checkCountLimit() { + guard options.cacheLimit > 0 else { + return + } - // update if its already stored - if try record.exists(db) { - try record.update(db) + context.perform { [weak self] in + guard let strongSelf = self else { return } - // check limit and delete if necessary - if let limit = self?.options.cacheLimit, limit > 0 { - let count = try UploadDataRecord.fetchCount(db) + do { + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + let count = try strongSelf.context.count(for: request) - if count >= limit { - let recordsToDelete = try UploadDataRecord - .order(Column("date").asc) - .limit(Int(limit)) - .fetchAll(db) + if count >= strongSelf.options.cacheLimit { + request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] + request.fetchLimit = max(0, count - Int(strongSelf.options.cacheLimit) + 10) - for recordToDelete in recordsToDelete { - try recordToDelete.delete(db) + let result = try strongSelf.context.fetch(request) + for uploadData in result { + strongSelf.context.delete(uploadData) } - } - } - try record.insert(db) + strongSelf.save() + } + } catch { } } } @@ -122,21 +201,19 @@ class EmbraceUploadCache { /// - Parameters: /// - id: Identifier of the data /// - type: Type of the data - /// - Returns: Boolean indicating if the data was successfully deleted - @discardableResult func deleteUploadData(id: String, type: EmbraceUploadType) throws -> Bool { - guard let uploadData = try fetchUploadData(id: id, type: type) else { - return false + func deleteUploadData(id: String, type: EmbraceUploadType) { + guard let uploadData = fetchUploadData(id: id, type: type) else { + return } - return try deleteUploadData(uploadData) + deleteUploadData(uploadData) } /// Deletes the cached `UploadDataRecord`. /// - Parameter uploadData: `UploadDataRecord` to delete - /// - Returns: Boolean indicating if the data was successfully deleted - func deleteUploadData(_ uploadData: UploadDataRecord) throws -> Bool { - try dbQueue.write { db in - return try uploadData.delete(db) + func deleteUploadData(_ uploadData: UploadDataRecord) { + context.perform { [weak self] in + self?.context.delete(uploadData) } } @@ -150,104 +227,40 @@ class EmbraceUploadCache { id: String, type: EmbraceUploadType, attemptCount: Int - ) throws { - try dbQueue.write { db in - let filter = UploadDataRecord.Schema.id == id && UploadDataRecord.Schema.type == type - try UploadDataRecord.filter(filter) - .updateAll(db, UploadDataRecord.Schema.attemptCount.set(to: attemptCount)) + ) { + guard let uploadData = fetchUploadData(id: id, type: type) else { + return } - } - - /// Fetches all records that should be deleted based on them being older than __maxDays__ days - /// - Parameter db: The database where to pull the data from, assumes the records to be UploadDataRecord. - /// - Parameter maxDays: The maximum allowed days old a record is allowed to be cached. - /// - Returns: An array of IDs from records that should be deleted. - func fetchRecordsToDeleteBasedOnDate(maxDays: UInt) throws -> [String] { - let sqlQuery = """ - SELECT id, date FROM uploads WHERE date <= DATE(DATE(), '-\(maxDays) day') - """ - - var result: [String] = [] - try dbQueue.read { db in - result = try String.fetchAll(db, sql: sqlQuery) + context.perform { [weak self] in + uploadData.attemptCount = attemptCount + self?.save() } - - return result } - /// Deletes requested records from the database based on their IDs - /// Assumes the records to be of type __UploadDataRecord__ - /// - Parameter recordIDs: The IDs array to delete - func deleteRecords(recordIDs: [String]) throws { - let questionMarks = "\(databaseQuestionMarks(count: recordIDs.count))" - let sqlQuery = "DELETE FROM uploads WHERE id IN (\(questionMarks))" - try dbQueue.write { db in - try db.execute(sql: sqlQuery, arguments: .init(recordIDs)) - } - } -} + /// Fetches all records that should be deleted based on them being older than the passed date + func fetchRecordsToDelete(dateLimit: Date) -> [UploadDataRecord] { -extension EmbraceUploadCache { + var result: [UploadDataRecord] = [] + context.performAndWait { + do { + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + request.predicate = NSPredicate(format: "date < %@", dateLimit as NSDate) - private static func createDBQueue( - options: EmbraceUpload.CacheOptions, - logger: InternalLogger - ) throws -> DatabaseQueue { - if case let .inMemory(name) = options.storageMechanism { - return try DatabaseQueue(named: name) - } else if case let .onDisk(baseURL, _) = options.storageMechanism, let fileURL = options.fileURL { - // create base directory if necessary - try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) - return try EmbraceUploadCache.getDBQueueIfPossible(at: fileURL, logger: logger) - } else { - fatalError("Unsupported storage mechansim added") + result = try context.fetch(request) + } catch { } } + return result } - /// Will attempt to create or open the DB File. If first attempt fails due to GRDB error, it'll assume the existing DB is corruped and try again after deleting the existing DB file. - private static func getDBQueueIfPossible(at fileURL: URL, logger: InternalLogger) throws -> DatabaseQueue { - do { - return try DatabaseQueue(path: fileURL.path) - } catch { - if let dbError = error as? DatabaseError { - logger.error( - """ - GRDB Failed to initialize EmbraceUploadCache. - Will attempt to remove existing file and create a new DB. - Message: \(dbError.message ?? "[empty message]"), - Result Code: \(dbError.resultCode), - SQLite Extended Code: \(dbError.extendedResultCode) - """ - ) - } else { - logger.error( - """ - Unknown error while trying to initialize EmbraceUploadCache: \(error) - Will attempt to recover by deleting existing DB. - """ - ) + /// Deletes requested records from the database + func deleteRecords(_ records: [UploadDataRecord]) { + context.perform { [weak self] in + for record in records { + self?.context.delete(record) } - } - - try EmbraceUploadCache.deleteDBFile(at: fileURL, logger: logger) - return try DatabaseQueue(path: fileURL.path) - } - - /// Will attempt to delete the provided file. - private static func deleteDBFile(at fileURL: URL, logger: InternalLogger) throws { - do { - let fileURL = URL(fileURLWithPath: fileURL.path) - try FileManager.default.removeItem(at: fileURL) - } catch let error { - logger.error( - """ - EmbraceUploadCache failed to remove DB file. - Error: \(error.localizedDescription) - Filepath: \(fileURL) - """ - ) + self?.save() } } } diff --git a/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift b/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift index fac9d683..bec417f2 100644 --- a/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift +++ b/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift @@ -4,50 +4,65 @@ import Foundation import GRDB +import CoreData /// Represents a cached upload data in the storage -public struct UploadDataRecord: Codable { - var id: String - var type: Int - var data: Data - var attemptCount: Int - var date: Date -} +public class UploadDataRecord: NSManagedObject { + @NSManaged var id: String + @NSManaged var type: Int + @NSManaged var data: Data + @NSManaged var attemptCount: Int + @NSManaged var date: Date -extension UploadDataRecord: FetchableRecord, PersistableRecord, MutablePersistableRecord { - public static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase - public static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase - public static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .replace, update: .replace) -} + class func create( + context: NSManagedObjectContext, + id: String, + type: Int, + data: Data, + attemptCount: + Int, + date: Date + ) -> UploadDataRecord { + let record = UploadDataRecord(context: context) + record.id = id + record.type = type + record.data = data + record.attemptCount = attemptCount + record.date = date -extension UploadDataRecord { - struct Schema { - static var id: Column { Column("id") } - static var type: Column { Column("type") } - static var data: Column { Column("data") } - static var attemptCount: Column { Column("attempt_count") } - static var date: Column { Column("date") } + return record } } -extension UploadDataRecord: TableRecord { - public static let databaseTableName: String = "uploads" +extension UploadDataRecord { + static let entityName = "UploadData" - internal static func defineTable(db: Database) throws { - try db.create(table: UploadDataRecord.databaseTableName, options: .ifNotExists) { t in - t.column(Schema.id.name, .text).notNull() - t.column(Schema.type.name, .integer).notNull() - t.primaryKey([Schema.id.name, Schema.type.name]) + static var entityDescription: NSEntityDescription { + let entity = NSEntityDescription() + entity.name = entityName + entity.managedObjectClassName = NSStringFromClass(UploadDataRecord.self) - t.column(Schema.data.name, .blob).notNull() - t.column(Schema.attemptCount.name, .integer).notNull() - t.column(Schema.date.name, .datetime).notNull() - } - } -} + let idAttribute = NSAttributeDescription() + idAttribute.name = "id" + idAttribute.attributeType = .stringAttributeType + + let typeAttribute = NSAttributeDescription() + typeAttribute.name = "type" + typeAttribute.attributeType = .integer64AttributeType + + let dataAttribute = NSAttributeDescription() + dataAttribute.name = "data" + dataAttribute.attributeType = .binaryDataAttributeType + + let attemptCountAttribute = NSAttributeDescription() + attemptCountAttribute.name = "attemptCount" + attemptCountAttribute.attributeType = .integer64AttributeType + + let dateAttribute = NSAttributeDescription() + dateAttribute.name = "date" + dateAttribute.attributeType = .dateAttributeType -extension UploadDataRecord: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.id == rhs.id && lhs.type == rhs.type + entity.properties = [idAttribute, typeAttribute, dataAttribute, attemptCountAttribute, dateAttribute] + return entity } } diff --git a/Sources/EmbraceUploadInternal/EmbraceUpload.swift b/Sources/EmbraceUploadInternal/EmbraceUpload.swift index 0079b938..5fafab82 100644 --- a/Sources/EmbraceUploadInternal/EmbraceUpload.swift +++ b/Sources/EmbraceUploadInternal/EmbraceUpload.swift @@ -79,7 +79,7 @@ public class EmbraceUpload: EmbraceLogUploader { // clear data from cache that shouldn't be retried as it's stale self.clearCacheFromStaleData() - + // get all the data cached first, is the only thing that could throw let cachedObjects = try self.cache.fetchAllUploadData() @@ -162,11 +162,16 @@ public class EmbraceUpload: EmbraceLogUploader { // cache operation let cacheOperation = BlockOperation { [weak self] in - do { - try self?.cache.saveUploadData(id: id, type: type, data: data) - completion?(.success(())) - } catch { - self?.logger.debug("Error caching upload data: \(error.localizedDescription)") + guard let strongSelf = self else { + return + } + + if strongSelf.cache.saveUploadData(id: id, type: type, data: data) { + completion?(.success(())) + } else { + strongSelf.logger.debug("Error caching upload data!") + + let error = NSError(domain: "com.embrace.upload", code: 5000) completion?(.failure(error)) } } @@ -244,11 +249,7 @@ public class EmbraceUpload: EmbraceLogUploader { case .failure(let isRetriable): if isRetriable, attemptCount < options.redundancy.maximumAmountOfRetries { operationQueue.addOperation { [weak self] in - do { - try self?.cache.updateAttemptCount(id: id, type: type, attemptCount: attemptCount) - } catch { - self?.logger.debug("Error updating cache: \(error.localizedDescription)") - } + self?.cache.updateAttemptCount(id: id, type: type, attemptCount: attemptCount) } return } @@ -259,13 +260,8 @@ public class EmbraceUpload: EmbraceLogUploader { private func addDeleteUploadDataOperation(id: String, type: EmbraceUploadType) { operationQueue.addOperation { [weak self] in - do { - try self?.cache.deleteUploadData(id: id, type: type) - } catch { - self?.logger.debug("Error deleting cache: \(error.localizedDescription)") - } + self?.cache.deleteUploadData(id: id, type: type) } - } private func clearCacheFromStaleData() { diff --git a/Sources/EmbraceUploadInternal/Operations/EmbraceUploadOperation.swift b/Sources/EmbraceUploadInternal/Operations/EmbraceUploadOperation.swift index 0f608bc4..5d8902d2 100644 --- a/Sources/EmbraceUploadInternal/Operations/EmbraceUploadOperation.swift +++ b/Sources/EmbraceUploadInternal/Operations/EmbraceUploadOperation.swift @@ -157,7 +157,7 @@ class EmbraceUploadOperation: AsyncOperation { // retry for all other non-handled cases with errors return error != nil } - + /// Extracts the suggested delay from `Retry-After` header from the `URLResponse` if present. /// - Parameter response: the URLResponse recevied when executing a request. /// - Returns:the time in seconds (as `Int`) extracted from the `Retry-After` header. diff --git a/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift b/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift index a1ec3240..15abca24 100644 --- a/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift +++ b/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift @@ -6,7 +6,7 @@ import Foundation public extension EmbraceUpload { enum StorageMechanism { - case inMemory(name: String) + case inMemory case onDisk(baseURL: URL, fileName: String) } @@ -40,7 +40,7 @@ public extension EmbraceUpload { cacheLimit: UInt = 0, cacheDaysLimit: UInt = 7 ) { - self.storageMechanism = .inMemory(name: named) + self.storageMechanism = .inMemory self.cacheLimit = cacheLimit self.cacheDaysLimit = cacheDaysLimit } @@ -48,14 +48,6 @@ public extension EmbraceUpload { } extension EmbraceUpload.CacheOptions { - /// The name of the storage item when using an inMemory storage - public var name: String? { - if case let .inMemory(name) = storageMechanism { - return name - } - return nil - } - /// URL pointing to the folder where the storage will be saved public var baseUrl: URL? { if case let .onDisk(baseURL, _) = storageMechanism { diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift index 91bd00a7..08eccd67 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift @@ -15,42 +15,48 @@ extension EmbraceUploadCacheTests { // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! let now = Date() - let record1 = UploadDataRecord( + let record1 = UploadDataRecord.create( + context: cache.context, id: "id1", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: Date(timeInterval: -1300, since: now) ) - let record2 = UploadDataRecord( + let record2 = UploadDataRecord.create( + context: cache.context, id: "id2", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate ) - let record3 = UploadDataRecord( + let record3 = UploadDataRecord.create( + context: cache.context, id: "id3", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate ) - let record4 = UploadDataRecord( + let record4 = UploadDataRecord.create( + context: cache.context, id: "id4", type: 0, data: Data(repeating: 3, count: 300), attemptCount: 0, date: Date(timeInterval: -1200, since: now) ) - let record5 = UploadDataRecord( + let record5 = UploadDataRecord.create( + context: cache.context, id: "id5", type: 0, data: Data(repeating: 3, count: 400), attemptCount: 0, date: Date(timeInterval: -1100, since: now) ) - let record6 = UploadDataRecord( + let record6 = UploadDataRecord.create( + context: cache.context, id: "id6", type: 0, data: Data(repeating: 3, count: 100), @@ -58,13 +64,10 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1000, since: now) ) - try cache.dbQueue.write { db in - try record1.insert(db) - try record2.insert(db) - try record3.insert(db) - try record4.insert(db) - try record5.insert(db) - try record6.insert(db) + cache.context.performAndWait { + do { + try cache.context.save() + } catch { } } // when attempting to remove data over the allowed days @@ -99,37 +102,43 @@ extension EmbraceUploadCacheTests { // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! let now = Date() - let record1 = UploadDataRecord( + let record1 = UploadDataRecord.create( + context: cache.context, id: "id1", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: Date(timeInterval: -1300, since: now) ) - let record2 = UploadDataRecord( + let record2 = UploadDataRecord.create( + context: cache.context, id: "id2", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate ) - let record3 = UploadDataRecord( + let record3 = UploadDataRecord.create( + context: cache.context, id: "id3", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate ) - let record4 = UploadDataRecord( + let record4 = UploadDataRecord.create( + context: cache.context, id: "id4", type: 0, data: Data(repeating: 3, count: 300), attemptCount: 0, date: Date(timeInterval: -1200, since: now) ) - let record5 = UploadDataRecord( + let record5 = UploadDataRecord.create( + context: cache.context, id: "id5", type: 0, data: Data(repeating: 3, count: 400), attemptCount: 0, date: Date(timeInterval: -1100, since: now) ) - let record6 = UploadDataRecord( + let record6 = UploadDataRecord.create( + context: cache.context, id: "id6", type: 0, data: Data(repeating: 3, count: 100), @@ -137,13 +146,10 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1000, since: now) ) - try cache.dbQueue.write { db in - try record1.insert(db) - try record2.insert(db) - try record3.insert(db) - try record4.insert(db) - try record5.insert(db) - try record6.insert(db) + cache.context.performAndWait { + do { + try cache.context.save() + } catch { } } // when attempting to remove data over the allowed days @@ -180,42 +186,48 @@ extension EmbraceUploadCacheTests { // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! let now = Date() - let record1 = UploadDataRecord( + let record1 = UploadDataRecord.create( + context: cache.context, id: "id1", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: Date(timeInterval: -1300, since: now) ) - let record2 = UploadDataRecord( + let record2 = UploadDataRecord.create( + context: cache.context, id: "id2", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate ) - let record3 = UploadDataRecord( + let record3 = UploadDataRecord.create( + context: cache.context, id: "id3", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate ) - let record4 = UploadDataRecord( + let record4 = UploadDataRecord.create( + context: cache.context, id: "id4", type: 0, data: Data(repeating: 3, count: 300), attemptCount: 0, date: Date(timeInterval: -1200, since: now) ) - let record5 = UploadDataRecord( + let record5 = UploadDataRecord.create( + context: cache.context, id: "id5", type: 0, data: Data(repeating: 3, count: 400), attemptCount: 0, date: Date(timeInterval: -1100, since: now) ) - let record6 = UploadDataRecord( + let record6 = UploadDataRecord.create( + context: cache.context, id: "id6", type: 0, data: Data(repeating: 3, count: 100), @@ -223,13 +235,10 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1000, since: now) ) - try cache.dbQueue.write { db in - try record1.insert(db) - try record2.insert(db) - try record3.insert(db) - try record4.insert(db) - try record5.insert(db) - try record6.insert(db) + cache.context.performAndWait { + do { + try cache.context.save() + } catch { } } // when attempting to remove data over the allowed days diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift index ada90c8a..18c89f4a 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift @@ -6,6 +6,7 @@ import XCTest import TestSupport import EmbraceOTelInternal @testable import EmbraceUploadInternal +import CoreData class EmbraceUploadCacheTests: XCTestCase { let logger = MockLogger() @@ -20,90 +21,24 @@ class EmbraceUploadCacheTests: XCTestCase { } - func test_tableSchema() throws { - // given new cache - let options = EmbraceUpload.CacheOptions(named: testName) - let cache = try EmbraceUploadCache(options: options, logger: logger) - - let expectation = XCTestExpectation() - - // then the table and its columns should be correct - try cache.dbQueue.read { db in - XCTAssert(try db.tableExists(UploadDataRecord.databaseTableName)) - - let columns = try db.columns(in: UploadDataRecord.databaseTableName) - - XCTAssert(try db.table(UploadDataRecord.databaseTableName, hasUniqueKey: ["id", "type"])) - - // id - let idColumn = columns.first(where: { $0.name == "id" }) - if let idColumn = idColumn { - XCTAssertEqual(idColumn.type, "TEXT") - XCTAssert(idColumn.isNotNull) - } else { - XCTAssert(false, "id column not found!") - } - - // type - let typeColumn = columns.first(where: { $0.name == "type" }) - if let typeColumn = typeColumn { - XCTAssertEqual(typeColumn.type, "INTEGER") - XCTAssert(typeColumn.isNotNull) - } else { - XCTAssert(false, "type column not found!") - } - - // data - let dataColumn = columns.first(where: { $0.name == "data" }) - if let dataColumn = dataColumn { - XCTAssertEqual(dataColumn.type, "BLOB") - XCTAssert(dataColumn.isNotNull) - } else { - XCTAssert(false, "data column not found!") - } - - // attemptCount - let attemptCountColumn = columns.first(where: { $0.name == "attempt_count" }) - if let attemptCountColumn = attemptCountColumn { - XCTAssertEqual(attemptCountColumn.type, "INTEGER") - XCTAssert(attemptCountColumn.isNotNull) - } else { - XCTAssert(false, "attempt_count column not found!") - } - - // date - let dateColumn = columns.first(where: { $0.name == "date" }) - if let dateColumn = dateColumn { - XCTAssertEqual(dateColumn.type, "DATETIME") - XCTAssert(dateColumn.isNotNull) - } else { - XCTAssert(false, "date column not found!") - } - - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) - } - func test_fetchUploadData() throws { let options = EmbraceUpload.CacheOptions(named: testName) let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data - let original = UploadDataRecord( + let original = UploadDataRecord.create( + context: cache.context, id: "id", type: EmbraceUploadType.spans.rawValue, data: Data(), attemptCount: 0, date: Date() ) - try cache.dbQueue.write { db in - try original.insert(db) - } + + cache.save() // when fetching the upload data - let uploadData = try cache.fetchUploadData(id: "id", type: .spans) + let uploadData = cache.fetchUploadData(id: "id", type: .spans) // then the upload data should be valid XCTAssertNotNil(uploadData) @@ -115,15 +50,32 @@ class EmbraceUploadCacheTests: XCTestCase { let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload datas - let data1 = UploadDataRecord(id: "id1", type: 0, data: Data(), attemptCount: 0, date: Date()) - let data2 = UploadDataRecord(id: "id2", type: 0, data: Data(), attemptCount: 0, date: Date()) - let data3 = UploadDataRecord(id: "id3", type: 0, data: Data(), attemptCount: 0, date: Date()) - - try cache.dbQueue.write { db in - try data1.insert(db) - try data2.insert(db) - try data3.insert(db) - } + let data1 = UploadDataRecord.create( + context: cache.context, + id: "id1", + type: 0, + data: Data(), + attemptCount: 0, + date: Date() + ) + let data2 = UploadDataRecord.create( + context: cache.context, + id: "id2", + type: 0, + data: Data(), + attemptCount: 0, + date: Date() + ) + let data3 = UploadDataRecord.create( + context: cache.context, + id: "id3", + type: 0, + data: Data(), + attemptCount: 0, + date: Date() + ) + + cache.save() // when fetching the upload datas let datas = try cache.fetchAllUploadData() @@ -139,13 +91,21 @@ class EmbraceUploadCacheTests: XCTestCase { let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data - let data = try cache.saveUploadData(id: "id", type: .spans, data: Data()) + _ = cache.saveUploadData(id: "id", type: .spans, data: Data()) // then the upload data should exist let expectation = XCTestExpectation() - try cache.dbQueue.read { db in - XCTAssert(try data.exists(db)) - expectation.fulfill() + + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + request.predicate = NSPredicate(format: "id == %@ AND type == %i", "id", EmbraceUploadType.spans.rawValue) + + cache.context.perform { + do { + let result = try cache.context.fetch(request) + if result.count > 0 { + expectation.fulfill() + } + } catch { } } wait(for: [expectation], timeout: .defaultTimeout) @@ -157,18 +117,23 @@ class EmbraceUploadCacheTests: XCTestCase { let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload datas - let data1 = try cache.saveUploadData(id: "id1", type: .spans, data: Data()) - let data2 = try cache.saveUploadData(id: "id2", type: .spans, data: Data()) - let data3 = try cache.saveUploadData(id: "id3", type: .spans, data: Data()) + _ = cache.saveUploadData(id: "id1", type: .spans, data: Data()) + _ = cache.saveUploadData(id: "id2", type: .spans, data: Data()) + _ = cache.saveUploadData(id: "id3", type: .spans, data: Data()) // then only the last data should exist let expectation = XCTestExpectation() - try cache.dbQueue.read { db in - XCTAssertFalse(try data1.exists(db)) - XCTAssertFalse(try data2.exists(db)) - XCTAssert(try data3.exists(db)) - expectation.fulfill() + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + + cache.context.perform { + do { + let result = try cache.context.fetch(request) + if result.count == 1, + result.first?.id == "id3" { + expectation.fulfill() + } + } catch { } } wait(for: [expectation], timeout: .defaultTimeout) @@ -179,26 +144,32 @@ class EmbraceUploadCacheTests: XCTestCase { let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data - let data = UploadDataRecord( + _ = UploadDataRecord.create( + context: cache.context, id: "id", type: EmbraceUploadType.spans.rawValue, data: Data(), attemptCount: 0, date: Date() ) - try cache.dbQueue.write { db in - try data.insert(db) - } + + cache.save() // when deleting the data - let success = try cache.deleteUploadData(id: "id", type: .spans) - XCTAssert(success) + cache.deleteUploadData(id: "id", type: .spans) // then the upload data should not exist let expectation = XCTestExpectation() - try cache.dbQueue.read { db in - XCTAssertFalse(try data.exists(db)) - expectation.fulfill() + + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + + cache.context.perform { + do { + let result = try cache.context.fetch(request) + if result.count == 0 { + expectation.fulfill() + } + } catch { } } wait(for: [expectation], timeout: .defaultTimeout) @@ -209,30 +180,34 @@ class EmbraceUploadCacheTests: XCTestCase { let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data - let original = UploadDataRecord( + _ = UploadDataRecord.create( + context: cache.context, id: "id", type: EmbraceUploadType.spans.rawValue, data: Data(), attemptCount: 0, date: Date() ) - try cache.dbQueue.write { db in - try original.insert(db) - } + + cache.save() // when updating the attempt count - _ = try cache.updateAttemptCount(id: "id", type: .spans, attemptCount: 10) + cache.updateAttemptCount(id: "id", type: .spans, attemptCount: 10) // then the data is updated successfully let expectation = XCTestExpectation() - try cache.dbQueue.read { db in - if let data = try UploadDataRecord.fetchOne(db) { - XCTAssertEqual(data.attemptCount, 10) - expectation.fulfill() - } else { - XCTAssert(false, "Invalid data!") - } + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + + cache.context.perform { + do { + let result = try cache.context.fetch(request) + if result.count == 1, + result.first?.id == "id", + result.first?.attemptCount == 10 { + expectation.fulfill() + } + } catch { } } wait(for: [expectation], timeout: .defaultTimeout) diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift index d77409e5..e72db368 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift @@ -102,24 +102,26 @@ class EmbraceUploadTests: XCTestCase { // given valid values let expectation1 = XCTestExpectation(description: "1. Data should be cached in the database") let expectation2 = XCTestExpectation(description: "2. Success completion callback should be called") - let expectation3 = XCTestExpectation(description: "4. Cache should be removed") + let expectation3 = XCTestExpectation(description: "3. Cache should be removed") var dataCached = false // then the data should be cached - let observation = ValueObservation.tracking(UploadDataRecord.fetchAll) - let cancellable = observation.start(in: module.cache.dbQueue) { error in - XCTAssert(false, error.localizedDescription) - } onChange: { records in - // and its data should be valid - if let record = records.first { - XCTAssertEqual(record.id, "id") - XCTAssertEqual(record.type, EmbraceUploadType.spans.rawValue) - XCTAssertEqual(record.data, TestConstants.data) - dataCached = true - expectation1.fulfill() - - // and it should be removed at the end - } else if dataCached { + let listener = CoreDataListener() + + listener.onInsertedObjects = { objects in + guard let record = objects.first as? UploadDataRecord else { + return + } + + XCTAssertEqual(record.id, "id") + XCTAssertEqual(record.type, EmbraceUploadType.spans.rawValue) + XCTAssertEqual(record.data, TestConstants.data) + dataCached = true + expectation1.fulfill() + } + + listener.onDeletedObjects = { objects in + if dataCached { expectation3.fulfill() } } @@ -139,9 +141,6 @@ class EmbraceUploadTests: XCTestCase { // the observability on the database seems to be inconsistent timing wise // so the first 2 steps are not always in the same order wait(for: [expectation1, expectation2, expectation3], timeout: .veryLongTimeout) - - // clean up - cancellable.cancel() } func test_cacheFlowOnError() throws { @@ -150,17 +149,17 @@ class EmbraceUploadTests: XCTestCase { let expectation2 = XCTestExpectation(description: "2. Sucess completion callback should be called") // then the data should be cached - let observation = ValueObservation.tracking(UploadDataRecord.fetchAll) - let cancellable = observation.start(in: module.cache.dbQueue) { error in - XCTAssert(false, error.localizedDescription) - } onChange: { records in - // and its data should be valid - if let record = records.first { - XCTAssertEqual(record.id, "id") - XCTAssertEqual(record.type, EmbraceUploadType.spans.rawValue) - XCTAssertEqual(record.data, TestConstants.data) - expectation1.fulfill() + let listener = CoreDataListener() + + listener.onInsertedObjects = { objects in + guard let record = objects.first as? UploadDataRecord else { + return } + + XCTAssertEqual(record.id, "id") + XCTAssertEqual(record.type, EmbraceUploadType.spans.rawValue) + XCTAssertEqual(record.data, TestConstants.data) + expectation1.fulfill() } // when uploading data @@ -177,17 +176,14 @@ class EmbraceUploadTests: XCTestCase { wait(for: [expectation1, expectation2], timeout: .veryLongTimeout) // the ndata should remain cached - let record = try module.cache.fetchUploadData(id: "id", type: .spans) + let record = module.cache.fetchUploadData(id: "id", type: .spans) XCTAssertNotNil(record) - - // clean up - cancellable.cancel() } func test_retryCachedData() throws { // given cached data - _ = try module.cache.saveUploadData(id: "id1", type: .spans, data: TestConstants.data) - _ = try module.cache.saveUploadData(id: "id2", type: .log, data: TestConstants.data) + _ = module.cache.saveUploadData(id: "id1", type: .spans, data: TestConstants.data) + _ = module.cache.saveUploadData(id: "id2", type: .log, data: TestConstants.data) EmbraceHTTPMock.mock(url: testSpansUrl()) EmbraceHTTPMock.mock(url: testLogsUrl()) diff --git a/Tests/TestSupport/CoreDataListener.swift b/Tests/TestSupport/CoreDataListener.swift new file mode 100644 index 00000000..66521516 --- /dev/null +++ b/Tests/TestSupport/CoreDataListener.swift @@ -0,0 +1,41 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import CoreData + +public class CoreDataListener { + + public var onInsertedObjects: ((Set) -> Void)? + public var onUpdatedObjects: ((Set) -> Void)? + public var onDeletedObjects: ((Set) -> Void)? + + public init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(contextObjectsDidChange(_:)), + name: Notification.Name.NSManagedObjectContextObjectsDidChange, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc func contextObjectsDidChange(_ notification: Notification) { + + if let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? Set, !insertedObjects.isEmpty { + onInsertedObjects?(insertedObjects) + } + + if let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set, !updatedObjects.isEmpty { + onUpdatedObjects?(updatedObjects) + } + + if let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set, !deletedObjects.isEmpty { + onDeletedObjects?(deletedObjects) + } + } +} From aa1376cc7047235b47931be70fe41ee12b436054 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Mon, 16 Dec 2024 15:57:49 -0300 Subject: [PATCH 02/17] Clean up --- Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift index e72db368..75b36bff 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift @@ -4,7 +4,6 @@ import XCTest import TestSupport -import GRDB @testable import EmbraceUploadInternal class EmbraceUploadTests: XCTestCase { From 2ec3eb1d041ee2fbef499f0bfa4ee71a190413a6 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Thu, 26 Dec 2024 14:55:27 -0300 Subject: [PATCH 03/17] Clean up --- .../EmbraceConfigurable/RemoteConfig.swift | 4 ++-- Sources/EmbraceCore/Internal/Embrace+Setup.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift index 5a0fc386..33cf5c39 100644 --- a/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift +++ b/Sources/EmbraceConfigInternal/EmbraceConfigurable/RemoteConfig.swift @@ -147,13 +147,13 @@ extension RemoteConfig { /// - digits: The number of digits used to calculate the total space. Must match the number of digits used to determine the hexValue /// - threshold: The percentage threshold to test against. Values between 0.0 and 100.0 static func isEnabled(hexValue: UInt64, digits: UInt, threshold: Float) -> Bool { - if threshold <= 0 || threshold > 100 { + guard threshold > 0 else { return false } let space = powf(16, Float(digits)) - 1 let result = (Float(hexValue) / space) * 100 - return result <= threshold + return result <= min(100, threshold) } } diff --git a/Sources/EmbraceCore/Internal/Embrace+Setup.swift b/Sources/EmbraceCore/Internal/Embrace+Setup.swift index afa8ab73..39811f50 100644 --- a/Sources/EmbraceCore/Internal/Embrace+Setup.swift +++ b/Sources/EmbraceCore/Internal/Embrace+Setup.swift @@ -67,7 +67,7 @@ extension Embrace { do { let options = EmbraceUpload.Options(endpoints: uploadEndpoints, cache: cache, metadata: metadata) - let queue = DispatchQueue(label: "com.embrace.upload", attributes: .concurrent) + let queue = DispatchQueue(label: "com.embrace.upload", qos: .background, attributes: .concurrent) return try EmbraceUpload(options: options, logger: Embrace.logger, queue: queue) } catch { From 523283224cef7b14b55853bdf2c9c02a99540a30 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Tue, 21 Jan 2025 13:34:53 -0300 Subject: [PATCH 04/17] Adding core data wrapper module --- Package.swift | 17 +++ .../Storage/StorageMechanism.swift | 52 +++++++ .../EmbraceCore/Internal/Embrace+Setup.swift | 10 +- .../CoreDataWrapper+Options.swift | 26 ++++ .../CoreDataWrapper.swift | 90 ++++++++++++ .../Cache/EmbraceUploadCache.swift | 132 ++++-------------- .../Options/EmbraceUpload+CacheOptions.swift | 50 +------ .../Session/SessionControllerTests.swift | 2 +- .../Session/UnsentDataHandlerTests.swift | 2 +- .../EmbraceUploadCacheOptionsTests.swift | 24 ---- ...mbraceUploadCacheTests+ClearDataDate.swift | 57 ++++---- .../EmbraceUploadCacheTests.swift | 54 +++---- .../EmbraceUploadTests.swift | 2 +- 13 files changed, 284 insertions(+), 234 deletions(-) create mode 100644 Sources/EmbraceCommonInternal/Storage/StorageMechanism.swift create mode 100644 Sources/EmbraceCoreDataInternal/CoreDataWrapper+Options.swift create mode 100644 Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift delete mode 100644 Tests/EmbraceUploadInternalTests/EmbraceUploadCacheOptionsTests.swift diff --git a/Package.swift b/Package.swift index fc9383a6..6a321f3c 100644 --- a/Package.swift +++ b/Package.swift @@ -208,6 +208,7 @@ let package = Package( dependencies: [ "EmbraceCommonInternal", "EmbraceOTelInternal", + "EmbraceCoreDataInternal", .product(name: "GRDB", package: "GRDB.swift") ] ), @@ -216,6 +217,22 @@ let package = Package( dependencies: [ "EmbraceUploadInternal", "EmbraceOTelInternal", + "EmbraceCoreDataInternal", + "TestSupport" + ] + ), + + // core data ----------------------------------------------------------------- + .target( + name: "EmbraceCoreDataInternal", + dependencies: [ + "EmbraceCommonInternal" + ] + ), + .testTarget( + name: "EmbraceCoreDataInternalTests", + dependencies: [ + "EmbraceCommonInternal", "TestSupport" ] ), diff --git a/Sources/EmbraceCommonInternal/Storage/StorageMechanism.swift b/Sources/EmbraceCommonInternal/Storage/StorageMechanism.swift new file mode 100644 index 00000000..72bbfb94 --- /dev/null +++ b/Sources/EmbraceCommonInternal/Storage/StorageMechanism.swift @@ -0,0 +1,52 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +public enum StorageMechanism { + case inMemory(name: String) + case onDisk(name: String, baseURL: URL) +} + +public extension StorageMechanism { + + /// Name identifier + var name: String { + switch self { + case .onDisk(let name, _): return name + case .inMemory(let name): return name + } + } + + /// URL pointing to the folder where the storage will be saved + var baseUrl: URL? { + switch self { + case .onDisk(_, let url): + return url + + default: return nil + } + } + + /// URL pointing to the folder where the storage will be saved + var fileName: String? { + switch self { + case .onDisk(let name, _): + return name + ".sqlite" + + default: return nil + } + } + + /// URL to the storage file + var fileURL: URL? { + switch self { + case .onDisk(let name, let url): + return url.appendingPathComponent(name + ".sqlite") + + default: return nil + } + } +} + diff --git a/Sources/EmbraceCore/Internal/Embrace+Setup.swift b/Sources/EmbraceCore/Internal/Embrace+Setup.swift index fe5d0f0a..f4e61e8a 100644 --- a/Sources/EmbraceCore/Internal/Embrace+Setup.swift +++ b/Sources/EmbraceCore/Internal/Embrace+Setup.swift @@ -56,13 +56,17 @@ extension Embrace { guard let cacheUrl = EmbraceFileSystem.uploadsDirectoryPath( partitionIdentifier: appId, appGroupId: options.appGroupId - ), - let cache = EmbraceUpload.CacheOptions(cacheBaseUrl: cacheUrl) - else { + ) else { Embrace.logger.error("Failed to initialize upload cache!") return nil } + let storageMechanism = StorageMechanism.onDisk( + name: "EmbraceUploadStorage", + baseURL: cacheUrl + ) + let cache = EmbraceUpload.CacheOptions(storageMechanism: storageMechanism) + // metadata let metadata = EmbraceUpload.MetadataOptions( apiKey: appId, diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper+Options.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper+Options.swift new file mode 100644 index 00000000..a491a3c8 --- /dev/null +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper+Options.swift @@ -0,0 +1,26 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import CoreData +import EmbraceCommonInternal + +public extension CoreDataWrapper { + + class Options { + /// Determines where the db is going to be stored + let storageMechanism: StorageMechanism + + /// Array on NSEntityDescriptions that define the db model + let entities: [NSEntityDescription] + + public init( + storageMechanism: StorageMechanism, + entities: [NSEntityDescription] + ) { + self.storageMechanism = storageMechanism + self.entities = entities + } + } +} diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift new file mode 100644 index 00000000..ef4917f8 --- /dev/null +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -0,0 +1,90 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import CoreData +import EmbraceCommonInternal + +public class CoreDataWrapper { + + public let options: CoreDataWrapper.Options + + let container: NSPersistentContainer + public let context: NSManagedObjectContext + + let logger: InternalLogger + + public init(options: CoreDataWrapper.Options, logger: InternalLogger) throws { + self.options = options + self.logger = logger + + // create model + let model = NSManagedObjectModel() + model.entities = options.entities + + // create container + let name = options.storageMechanism.name + self.container = NSPersistentContainer(name: name, managedObjectModel: model) + + switch options.storageMechanism { + case .inMemory(_): + let description = NSPersistentStoreDescription() + description.type = NSInMemoryStoreType + self.container.persistentStoreDescriptions = [description] + + case let .onDisk(_, baseURL): + try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) + let description = NSPersistentStoreDescription() + description.url = options.storageMechanism.fileURL + self.container.persistentStoreDescriptions = [description] + } + + container.loadPersistentStores { _, error in + if let error { + logger.error("Error initializing CoreData \"\(name)\": \(error.localizedDescription)") + } + } + + self.context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + self.context.persistentStoreCoordinator = self.container.persistentStoreCoordinator + } + + /// Synchronously saves all changes on the current context to disk + public func save() { + context.perform { [weak self] in + do { + try self?.context.save() + } catch { + self?.logger.warning("Erro saving EmbraceUploadCache: \(error.localizedDescription)") + } + } + } + + /// Synchronously fetches the records that satisfy the given request + public func fetch(withRequest request: NSFetchRequest) -> [T] where T : NSManagedObject { + var result: [T] = [] + context.performAndWait { + do { + result = try context.fetch(request) + } catch { } + } + return result + } + + /// Asynchronously deletes record from the database + public func deleteRecord(_ record: T) where T : NSManagedObject { + deleteRecords([record]) + } + + /// Asynchronously deletes requested records from the database + public func deleteRecords(_ records: [T]) where T : NSManagedObject { + context.perform { [weak self] in + for record in records { + self?.context.delete(record) + } + + self?.save() + } + } +} diff --git a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift index d86abcfa..3cde0014 100644 --- a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift +++ b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift @@ -5,14 +5,14 @@ import Foundation import EmbraceOTelInternal import EmbraceCommonInternal +import EmbraceCoreDataInternal import CoreData /// Class that handles all the cached upload data generated by the Embrace SDK. class EmbraceUploadCache { private(set) var options: EmbraceUpload.CacheOptions - let container: NSPersistentContainer - let context: NSManagedObjectContext + let coreData: CoreDataWrapper let logger: InternalLogger init(options: EmbraceUpload.CacheOptions, logger: InternalLogger) throws { @@ -20,43 +20,11 @@ class EmbraceUploadCache { self.logger = logger // create core data stack - let model = NSManagedObjectModel() - model.entities = [UploadDataRecord.entityDescription] - - self.container = NSPersistentContainer(name: "EmbraceUploadCache", managedObjectModel: model) - - switch options.storageMechanism { - case .inMemory: - let description = NSPersistentStoreDescription() - description.type = NSInMemoryStoreType - self.container.persistentStoreDescriptions = [description] - - case let .onDisk(baseURL, _): - try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) - let description = NSPersistentStoreDescription() - description.url = options.fileURL - self.container.persistentStoreDescriptions = [description] - } - - container.loadPersistentStores { _, error in - if let error { - logger.error("Error initializing EmbraceUpload cache!: \(error.localizedDescription)") - } - } - - self.context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - self.context.persistentStoreCoordinator = self.container.persistentStoreCoordinator - } - - // Saves all changes on the current context to disk - func save() { - context.perform { [weak self] in - do { - try self?.context.save() - } catch { - self?.logger.warning("Erro saving EmbraceUploadCache: \(error.localizedDescription)") - } - } + let coreDataOptions = CoreDataWrapper.Options( + storageMechanism: options.storageMechanism, + entities: [UploadDataRecord.entityDescription] + ) + self.coreData = try CoreDataWrapper(options: coreDataOptions, logger: logger) } /// Fetches the cached upload data for the given identifier. @@ -65,31 +33,17 @@ class EmbraceUploadCache { /// - type: Type of the data /// - Returns: The cached `UploadDataRecord`, if any public func fetchUploadData(id: String, type: EmbraceUploadType) -> UploadDataRecord? { + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + request.predicate = NSPredicate(format: "id == %@ AND type == %i", id, type.rawValue) - var result: UploadDataRecord? - context.performAndWait { - do { - let request = NSFetchRequest(entityName: UploadDataRecord.entityName) - request.predicate = NSPredicate(format: "id == %@ AND type == %i", id, type.rawValue) - - result = try context.fetch(request).first - } catch { } - } - return result + return coreData.fetch(withRequest: request).first } /// Fetches all the cached upload data. /// - Returns: An array containing all the cached `UploadDataRecords` public func fetchAllUploadData() throws -> [UploadDataRecord] { - - var result: [UploadDataRecord] = [] - context.performAndWait { - do { - let request = NSFetchRequest(entityName: UploadDataRecord.entityName) - result = try context.fetch(request) - } catch { } - } - return result + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + return coreData.fetch(withRequest: request) } /// Removes stale data based on size or date, if they're limited in options. @@ -112,7 +66,7 @@ class EmbraceUploadCache { span.setStartTime(time: Date()) let startedSpan = span.startSpan() - deleteRecords(recordsToDelete) + coreData.deleteRecords(recordsToDelete) startedSpan.end() return UInt(deleteCount) @@ -131,9 +85,9 @@ class EmbraceUploadCache { // update if it already exists if let record = fetchUploadData(id: id, type: type) { - context.perform { [weak self] in + coreData.context.perform { [weak self] in record.data = data - self?.save() + self?.coreData.save() } return true @@ -145,9 +99,9 @@ class EmbraceUploadCache { // insert new var result = true - context.performAndWait { + coreData.context.performAndWait { let record = UploadDataRecord.create( - context: context, + context: coreData.context, id: id, type: type.rawValue, data: data, @@ -156,9 +110,9 @@ class EmbraceUploadCache { ) do { - try context.save() + try coreData.context.save() } catch { - context.delete(record) + coreData.context.delete(record) result = false } } @@ -173,25 +127,25 @@ class EmbraceUploadCache { return } - context.perform { [weak self] in + coreData.context.perform { [weak self] in guard let strongSelf = self else { return } do { let request = NSFetchRequest(entityName: UploadDataRecord.entityName) - let count = try strongSelf.context.count(for: request) + let count = try strongSelf.coreData.context.count(for: request) if count >= strongSelf.options.cacheLimit { request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)] request.fetchLimit = max(0, count - Int(strongSelf.options.cacheLimit) + 10) - let result = try strongSelf.context.fetch(request) + let result = try strongSelf.coreData.context.fetch(request) for uploadData in result { - strongSelf.context.delete(uploadData) + strongSelf.coreData.context.delete(uploadData) } - strongSelf.save() + strongSelf.coreData.save() } } catch { } } @@ -206,15 +160,7 @@ class EmbraceUploadCache { return } - deleteUploadData(uploadData) - } - - /// Deletes the cached `UploadDataRecord`. - /// - Parameter uploadData: `UploadDataRecord` to delete - func deleteUploadData(_ uploadData: UploadDataRecord) { - context.perform { [weak self] in - self?.context.delete(uploadData) - } + coreData.deleteRecord(uploadData) } /// Updates the attempt count of the upload data for the given identifier. @@ -232,35 +178,17 @@ class EmbraceUploadCache { return } - context.perform { [weak self] in + coreData.context.perform { [weak self] in uploadData.attemptCount = attemptCount - self?.save() + self?.coreData.save() } } /// Fetches all records that should be deleted based on them being older than the passed date func fetchRecordsToDelete(dateLimit: Date) -> [UploadDataRecord] { + let request = NSFetchRequest(entityName: UploadDataRecord.entityName) + request.predicate = NSPredicate(format: "date < %@", dateLimit as NSDate) - var result: [UploadDataRecord] = [] - context.performAndWait { - do { - let request = NSFetchRequest(entityName: UploadDataRecord.entityName) - request.predicate = NSPredicate(format: "date < %@", dateLimit as NSDate) - - result = try context.fetch(request) - } catch { } - } - return result - } - - /// Deletes requested records from the database - func deleteRecords(_ records: [UploadDataRecord]) { - context.perform { [weak self] in - for record in records { - self?.context.delete(record) - } - - self?.save() - } + return coreData.fetch(withRequest: request) } } diff --git a/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift b/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift index 5556d0aa..c89d868a 100644 --- a/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift +++ b/Sources/EmbraceUploadInternal/Options/EmbraceUpload+CacheOptions.swift @@ -3,12 +3,9 @@ // import Foundation +import EmbraceCommonInternal public extension EmbraceUpload { - enum StorageMechanism { - case inMemory - case onDisk(baseURL: URL, fileName: String) - } class CacheOptions { /// Determines where the db is going to be @@ -20,55 +17,14 @@ public extension EmbraceUpload { /// Determines the maximum amount of days a request will be cached. Use 0 to disable. public let cacheDaysLimit: UInt - public init?( - cacheBaseUrl: URL, - cacheFileName: String = "db.sqlite", - cacheLimit: UInt = 0, - cacheDaysLimit: UInt = 7 - ) { - if !cacheBaseUrl.isFileURL { - return nil - } - - self.storageMechanism = .onDisk(baseURL: cacheBaseUrl, fileName: cacheFileName) - self.cacheLimit = cacheLimit - self.cacheDaysLimit = cacheDaysLimit - } - public init( - named: String, + storageMechanism: StorageMechanism, cacheLimit: UInt = 0, cacheDaysLimit: UInt = 7 ) { - self.storageMechanism = .inMemory + self.storageMechanism = storageMechanism self.cacheLimit = cacheLimit self.cacheDaysLimit = cacheDaysLimit } } } - -extension EmbraceUpload.CacheOptions { - /// URL pointing to the folder where the storage will be saved - public var baseUrl: URL? { - if case let .onDisk(baseURL, _) = storageMechanism { - return baseURL - } - return nil - } - - /// URL pointing to the folder where the storage will be saved - public var fileName: String? { - if case let .onDisk(_, name) = storageMechanism { - return name - } - return nil - } - - /// URL to the storage file - public var fileURL: URL? { - if case let .onDisk(url, filename) = storageMechanism { - return url.appendingPathComponent(filename) - } - return nil - } -} diff --git a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift index b3b2abb9..dbe0c027 100644 --- a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift +++ b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift @@ -39,7 +39,7 @@ final class SessionControllerTests: XCTestCase { uploadTestOptions = EmbraceUpload.Options( endpoints: testEndpointOptions(testName: testName), - cache: EmbraceUpload.CacheOptions(named: testName), + cache: EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName)), metadata: Self.testMetadataOptions, redundancy: Self.testRedundancyOptions, urlSessionConfiguration: uploadUrlSessionconfig diff --git a/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift b/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift index dfeb64b4..89814d48 100644 --- a/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift @@ -43,7 +43,7 @@ class UnsentDataHandlerTests: XCTestCase { uploadOptions = EmbraceUpload.Options( endpoints: testEndpointOptions(forTest: testName), - cache: EmbraceUpload.CacheOptions(named: testName), + cache: EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName)), metadata: UnsentDataHandlerTests.testMetadataOptions, redundancy: UnsentDataHandlerTests.testRedundancyOptions, urlSessionConfiguration: urlSessionconfig diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheOptionsTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheOptionsTests.swift deleted file mode 100644 index f92aab1b..00000000 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheOptionsTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest -@testable import EmbraceUploadInternal - -class EmbraceUploadCacheOptionsTests: XCTestCase { - - func test_validCacheBaseUrl() { - let url = URL(fileURLWithPath: NSTemporaryDirectory()) - let options = EmbraceUpload.CacheOptions(cacheBaseUrl: url) - - XCTAssertNotNil(options) - } - - func test_invalidCacheBaseUrl() { - if let url = URL(string: "https://embrace.io/") { - let options = EmbraceUpload.CacheOptions(cacheBaseUrl: url) - - XCTAssertNil(options) - } - } -} diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift index 08eccd67..723bb374 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift @@ -5,18 +5,19 @@ import XCTest import TestSupport @testable import EmbraceUploadInternal +import EmbraceCommonInternal extension EmbraceUploadCacheTests { func test_clearStaleDataIfNeeded_basedOn_date() throws { // setting the maximum allowed days - let options = EmbraceUpload.CacheOptions(named: testName, cacheDaysLimit: 15) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: (testName)), cacheDaysLimit: 15) let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! let now = Date() let record1 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id1", type: 0, data: Data(repeating: 3, count: 1), @@ -24,7 +25,7 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1300, since: now) ) let record2 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id2", type: 0, data: Data(repeating: 3, count: 1), @@ -32,7 +33,7 @@ extension EmbraceUploadCacheTests { date: oldDate ) let record3 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id3", type: 0, data: Data(repeating: 3, count: 1), @@ -40,7 +41,7 @@ extension EmbraceUploadCacheTests { date: oldDate ) let record4 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id4", type: 0, data: Data(repeating: 3, count: 300), @@ -48,7 +49,7 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1200, since: now) ) let record5 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id5", type: 0, data: Data(repeating: 3, count: 400), @@ -56,7 +57,7 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1100, since: now) ) let record6 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id6", type: 0, data: Data(repeating: 3, count: 100), @@ -64,9 +65,9 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1000, since: now) ) - cache.context.performAndWait { + cache.coreData.context.performAndWait { do { - try cache.context.save() + try cache.coreData.context.save() } catch { } } @@ -96,49 +97,49 @@ extension EmbraceUploadCacheTests { func test_clearStaleDataIfNeeded_basedOn_date_noLimit() throws { // disabling maximum allowed days - let options = EmbraceUpload.CacheOptions(named: testName, cacheDaysLimit: 0) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName), cacheDaysLimit: 0) let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! let now = Date() let record1 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id1", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: Date(timeInterval: -1300, since: now) ) let record2 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id2", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate ) let record3 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id3", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate ) let record4 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id4", type: 0, data: Data(repeating: 3, count: 300), attemptCount: 0, date: Date(timeInterval: -1200, since: now) ) let record5 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id5", type: 0, data: Data(repeating: 3, count: 400), attemptCount: 0, date: Date(timeInterval: -1100, since: now) ) let record6 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id6", type: 0, data: Data(repeating: 3, count: 100), @@ -146,9 +147,9 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1000, since: now) ) - cache.context.performAndWait { + cache.coreData.context.performAndWait { do { - try cache.context.save() + try cache.coreData.context.save() } catch { } } @@ -168,7 +169,7 @@ extension EmbraceUploadCacheTests { func test_clearStaleDataIfNeeded_basedOn_date_noRecords() throws { // setting minimum allowed time - let options = EmbraceUpload.CacheOptions(named: testName, cacheDaysLimit: 1) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName), cacheDaysLimit: 1) let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // when attempting to remove data from an empty cache @@ -180,14 +181,14 @@ extension EmbraceUploadCacheTests { func test_clearStaleDataIfNeeded_basedOn_date_didNotHitTimeLimit() throws { // disabling maximum allowed days - let options = EmbraceUpload.CacheOptions(named: testName, cacheDaysLimit: 17) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName), cacheDaysLimit: 17) let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // given some upload cache let oldDate = Calendar.current.date(byAdding: .day, value: -16, to: Date())! let now = Date() let record1 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id1", type: 0, data: Data(repeating: 3, count: 1), @@ -195,7 +196,7 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1300, since: now) ) let record2 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id2", type: 0, data: Data(repeating: 3, count: 1), @@ -203,7 +204,7 @@ extension EmbraceUploadCacheTests { date: oldDate ) let record3 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id3", type: 0, data: Data(repeating: 3, count: 1), @@ -211,7 +212,7 @@ extension EmbraceUploadCacheTests { date: oldDate ) let record4 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id4", type: 0, data: Data(repeating: 3, count: 300), @@ -219,7 +220,7 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1200, since: now) ) let record5 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id5", type: 0, data: Data(repeating: 3, count: 400), @@ -227,7 +228,7 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1100, since: now) ) let record6 = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id6", type: 0, data: Data(repeating: 3, count: 100), @@ -235,9 +236,9 @@ extension EmbraceUploadCacheTests { date: Date(timeInterval: -1000, since: now) ) - cache.context.performAndWait { + cache.coreData.context.performAndWait { do { - try cache.context.save() + try cache.coreData.context.save() } catch { } } diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift index 18c89f4a..4d1cb02f 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift @@ -22,12 +22,12 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_fetchUploadData() throws { - let options = EmbraceUpload.CacheOptions(named: testName) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName)) let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data let original = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id", type: EmbraceUploadType.spans.rawValue, data: Data(), @@ -35,7 +35,7 @@ class EmbraceUploadCacheTests: XCTestCase { date: Date() ) - cache.save() + cache.coreData.save() // when fetching the upload data let uploadData = cache.fetchUploadData(id: "id", type: .spans) @@ -46,36 +46,36 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_fetchAllUploadData() throws { - let options = EmbraceUpload.CacheOptions(named: testName) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName)) let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload datas let data1 = UploadDataRecord.create( - context: cache.context, - id: "id1", + context: cache.coreData.context, + id: "id1", type: 0, data: Data(), attemptCount: 0, date: Date() ) let data2 = UploadDataRecord.create( - context: cache.context, - id: "id2", + context: cache.coreData.context, + id: "id2", type: 0, data: Data(), attemptCount: 0, date: Date() ) let data3 = UploadDataRecord.create( - context: cache.context, - id: "id3", + context: cache.coreData.context, + id: "id3", type: 0, data: Data(), attemptCount: 0, date: Date() ) - cache.save() + cache.coreData.save() // when fetching the upload datas let datas = try cache.fetchAllUploadData() @@ -87,7 +87,7 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_saveUploadData() throws { - let options = EmbraceUpload.CacheOptions(named: testName) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName)) let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data @@ -99,9 +99,9 @@ class EmbraceUploadCacheTests: XCTestCase { let request = NSFetchRequest(entityName: UploadDataRecord.entityName) request.predicate = NSPredicate(format: "id == %@ AND type == %i", "id", EmbraceUploadType.spans.rawValue) - cache.context.perform { + cache.coreData.context.perform { do { - let result = try cache.context.fetch(request) + let result = try cache.coreData.context.fetch(request) if result.count > 0 { expectation.fulfill() } @@ -113,7 +113,7 @@ class EmbraceUploadCacheTests: XCTestCase { func test_saveUploadData_limit() throws { // given a cache with a limit of 1 - let options = EmbraceUpload.CacheOptions(named: testName, cacheLimit: 1) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName), cacheLimit: 1) let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload datas @@ -126,9 +126,9 @@ class EmbraceUploadCacheTests: XCTestCase { let request = NSFetchRequest(entityName: UploadDataRecord.entityName) - cache.context.perform { + cache.coreData.context.perform { do { - let result = try cache.context.fetch(request) + let result = try cache.coreData.context.fetch(request) if result.count == 1, result.first?.id == "id3" { expectation.fulfill() @@ -140,12 +140,12 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_deleteUploadData() throws { - let options = EmbraceUpload.CacheOptions(named: testName) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName)) let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data _ = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id", type: EmbraceUploadType.spans.rawValue, data: Data(), @@ -153,7 +153,7 @@ class EmbraceUploadCacheTests: XCTestCase { date: Date() ) - cache.save() + cache.coreData.save() // when deleting the data cache.deleteUploadData(id: "id", type: .spans) @@ -163,9 +163,9 @@ class EmbraceUploadCacheTests: XCTestCase { let request = NSFetchRequest(entityName: UploadDataRecord.entityName) - cache.context.perform { + cache.coreData.context.perform { do { - let result = try cache.context.fetch(request) + let result = try cache.coreData.context.fetch(request) if result.count == 0 { expectation.fulfill() } @@ -176,12 +176,12 @@ class EmbraceUploadCacheTests: XCTestCase { } func test_updateAttemptCount() throws { - let options = EmbraceUpload.CacheOptions(named: testName) + let options = EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName)) let cache = try EmbraceUploadCache(options: options, logger: logger) // given inserted upload data _ = UploadDataRecord.create( - context: cache.context, + context: cache.coreData.context, id: "id", type: EmbraceUploadType.spans.rawValue, data: Data(), @@ -189,7 +189,7 @@ class EmbraceUploadCacheTests: XCTestCase { date: Date() ) - cache.save() + cache.coreData.save() // when updating the attempt count cache.updateAttemptCount(id: "id", type: .spans, attemptCount: 10) @@ -199,9 +199,9 @@ class EmbraceUploadCacheTests: XCTestCase { let request = NSFetchRequest(entityName: UploadDataRecord.entityName) - cache.context.perform { + cache.coreData.context.perform { do { - let result = try cache.context.fetch(request) + let result = try cache.coreData.context.fetch(request) if result.count == 1, result.first?.id == "id", result.first?.attemptCount == 10 { diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift index 1fa3e849..5efe53c5 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadTests.swift @@ -25,7 +25,7 @@ class EmbraceUploadTests: XCTestCase { testOptions = EmbraceUpload.Options( endpoints: testEndpointOptions(testName: testName), - cache: EmbraceUpload.CacheOptions(named: testName), + cache: EmbraceUpload.CacheOptions(storageMechanism: .inMemory(name: testName)), metadata: EmbraceUploadTests.testMetadataOptions, redundancy: EmbraceUploadTests.testRedundancyOptions, urlSessionConfiguration: urlSessionconfig From 510433eb6333c2d1241e8d911e65a3ec7235c1c6 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Wed, 22 Jan 2025 15:05:16 -0300 Subject: [PATCH 05/17] Adding temporal core data clone for metadata --- .../xcschemes/EmbraceIO-Package.xcscheme | 10 ++ .../Storage/StorageMechanism.swift | 1 - .../Metadata/MetadataHandler+User.swift | 10 +- .../Public/Metadata/MetadataHandler.swift | 67 ++++++++++++- .../Public/Metadata/MetadataRecordTmp.swift | 86 +++++++++++++++++ .../CoreDataWrapper.swift | 10 +- .../EmbraceStorage+Options.swift | 2 +- .../Records/EmbraceStorage+Metadata.swift | 6 +- .../Cache/EmbraceUploadCache.swift | 5 + .../CoreDataWrapperTests.swift | 94 +++++++++++++++++++ .../Metadata/MetadataHandlerTests.swift | 26 +++++ .../MetadataRecordTests.swift | 2 +- 12 files changed, 299 insertions(+), 20 deletions(-) create mode 100644 Sources/EmbraceCore/Public/Metadata/MetadataRecordTmp.swift create mode 100644 Tests/EmbraceCoreDataInternalTests/CoreDataWrapperTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EmbraceIO-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EmbraceIO-Package.xcscheme index c343d4e4..2af75924 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/EmbraceIO-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/EmbraceIO-Package.xcscheme @@ -644,6 +644,16 @@ ReferencedContainer = "container:"> + + + + (entityName: MetadataRecordTmp.entityName) + let oldRecords = coreData.fetch(withRequest: request) + coreData.deleteRecords(oldRecords) + + do { + var newRecords: [MetadataRecord] = [] + try storage.dbQueue.read { db in + newRecords = try MetadataRecord.fetchAll(db) + } + + coreData.context.perform { + for record in newRecords { + _ = MetadataRecordTmp.create(context: coreData.context, record: record) + } + + do { + try coreData.context.save() + } catch { + Embrace.logger.error("Error saving metadata core data!:\n\(error.localizedDescription)") + } + } + } catch { + Embrace.logger.error("Error cloning metadata!:\n\(error.localizedDescription)") + } + } +} diff --git a/Sources/EmbraceCore/Public/Metadata/MetadataRecordTmp.swift b/Sources/EmbraceCore/Public/Metadata/MetadataRecordTmp.swift new file mode 100644 index 00000000..75da1d8e --- /dev/null +++ b/Sources/EmbraceCore/Public/Metadata/MetadataRecordTmp.swift @@ -0,0 +1,86 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import EmbraceStorageInternal +import CoreData + +public class MetadataRecordTmp: NSManagedObject { + @NSManaged var key: String + @NSManaged var value: String + @NSManaged var type: String + @NSManaged var lifespan: String + @NSManaged var lifespanId: String + @NSManaged var collectedAt: Date + + class func create( + context: NSManagedObjectContext, + key: String, + value: String, + type: String, + lifespan: String, + lifespanId: String, + collectedAt: Date = Date() + ) -> MetadataRecordTmp { + let record = MetadataRecordTmp(context: context) + record.key = key + record.value = value + record.type = type + record.lifespan = lifespan + record.lifespanId = lifespanId + record.collectedAt = collectedAt + + return record + } + + class func create(context: NSManagedObjectContext, record: MetadataRecord) -> MetadataRecordTmp { + return create( + context: context, + key: record.key, + value: record.value.description, + type: record.type.rawValue, + lifespan: record.lifespan.rawValue, + lifespanId: record.lifespanId, + collectedAt: record.collectedAt + ) + } +} + +extension MetadataRecordTmp { + static let entityName = "MetadataRecordTmp" + + static var entityDescription: NSEntityDescription { + let entity = NSEntityDescription() + entity.name = entityName + entity.managedObjectClassName = NSStringFromClass(MetadataRecordTmp.self) + + let keyAttribute = NSAttributeDescription() + keyAttribute.name = "key" + keyAttribute.attributeType = .stringAttributeType + + let valueAttribute = NSAttributeDescription() + valueAttribute.name = "value" + valueAttribute.attributeType = .stringAttributeType + + let typeAttribute = NSAttributeDescription() + typeAttribute.name = "type" + typeAttribute.attributeType = .stringAttributeType + + let lifespanAttribute = NSAttributeDescription() + lifespanAttribute.name = "lifespan" + lifespanAttribute.attributeType = .stringAttributeType + + let lifespanIdAttribute = NSAttributeDescription() + lifespanIdAttribute.name = "lifespanId" + lifespanIdAttribute.attributeType = .stringAttributeType + + let dateAttribute = NSAttributeDescription() + dateAttribute.name = "collectedAt" + dateAttribute.attributeType = .dateAttributeType + + entity.properties = [keyAttribute, valueAttribute, typeAttribute, lifespanAttribute, lifespanIdAttribute, dateAttribute] + return entity + } +} diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index ef4917f8..41301b29 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -7,7 +7,7 @@ import CoreData import EmbraceCommonInternal public class CoreDataWrapper { - + public let options: CoreDataWrapper.Options let container: NSPersistentContainer @@ -28,7 +28,7 @@ public class CoreDataWrapper { self.container = NSPersistentContainer(name: name, managedObjectModel: model) switch options.storageMechanism { - case .inMemory(_): + case .inMemory: let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType self.container.persistentStoreDescriptions = [description] @@ -62,7 +62,7 @@ public class CoreDataWrapper { } /// Synchronously fetches the records that satisfy the given request - public func fetch(withRequest request: NSFetchRequest) -> [T] where T : NSManagedObject { + public func fetch(withRequest request: NSFetchRequest) -> [T] where T: NSManagedObject { var result: [T] = [] context.performAndWait { do { @@ -73,12 +73,12 @@ public class CoreDataWrapper { } /// Asynchronously deletes record from the database - public func deleteRecord(_ record: T) where T : NSManagedObject { + public func deleteRecord(_ record: T) where T: NSManagedObject { deleteRecords([record]) } /// Asynchronously deletes requested records from the database - public func deleteRecords(_ records: [T]) where T : NSManagedObject { + public func deleteRecords(_ records: [T]) where T: NSManagedObject { context.perform { [weak self] in for record in records { self?.context.delete(record) diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift b/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift index 31e53385..c3f58b1e 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift @@ -14,7 +14,7 @@ public extension EmbraceStorage { /// Class used to configure a EmbraceStorage instance class Options { /// Determines where the db is going to be - let storageMechanism: StorageMechanism + public let storageMechanism: StorageMechanism /// Dictionary containing the storage limits per span type public var spanLimits: [SpanType: Int] = [:] diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift index 270f6d4f..3e9fe3e1 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift @@ -91,7 +91,8 @@ extension EmbraceStorage { key: String, value: String, type: MetadataRecordType, - lifespan: MetadataRecordLifespan + lifespan: MetadataRecordLifespan, + lifespanId: String ) throws { try dbQueue.write { db in @@ -99,7 +100,8 @@ extension EmbraceStorage { .filter( MetadataRecord.Schema.key == key && MetadataRecord.Schema.type == type.rawValue && - MetadataRecord.Schema.lifespan == lifespan.rawValue + MetadataRecord.Schema.lifespan == lifespan.rawValue && + MetadataRecord.Schema.lifespanId == lifespanId ) .fetchOne(db) else { return diff --git a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift index 3cde0014..66061901 100644 --- a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift +++ b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift @@ -19,6 +19,11 @@ class EmbraceUploadCache { self.options = options self.logger = logger + // remove old GRDB sqlite file + if let url = options.storageMechanism.baseUrl?.appendingPathComponent("db.sqlite") { + try? FileManager.default.removeItem(at: url) + } + // create core data stack let coreDataOptions = CoreDataWrapper.Options( storageMechanism: options.storageMechanism, diff --git a/Tests/EmbraceCoreDataInternalTests/CoreDataWrapperTests.swift b/Tests/EmbraceCoreDataInternalTests/CoreDataWrapperTests.swift new file mode 100644 index 00000000..51d81da0 --- /dev/null +++ b/Tests/EmbraceCoreDataInternalTests/CoreDataWrapperTests.swift @@ -0,0 +1,94 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import CoreData +import XCTest +@testable import EmbraceCoreDataInternal +import EmbraceCommonInternal +import TestSupport + +class CoreDataWrapperTests: XCTestCase { + + var wrapper: CoreDataWrapper! + + override func setUpWithError() throws { + let storageMechanism: StorageMechanism = .inMemory(name: testName) + let options = CoreDataWrapper.Options(storageMechanism: storageMechanism, entities: [MockRecord.entityDescription]) + try wrapper = CoreDataWrapper(options: options, logger: MockLogger()) + } + + func test_fetch() throws { + // given a wrapper with data + _ = MockRecord.create(context: wrapper.context, id: "test") + wrapper.save() + + // when fetching data + let request = NSFetchRequest(entityName: MockRecord.entityName) + request.predicate = NSPredicate(format: "id == %@", "test") + + let result = wrapper.fetch(withRequest: request) + + // then the data is correct + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first!.id, "test") + + } + + func test_deleteRecord() throws { + // given a wrapper with data + let record = MockRecord.create(context: wrapper.context, id: "test") + wrapper.save() + + // when deleting the record + wrapper.deleteRecord(record) + + // then the record is deleted + let request = NSFetchRequest(entityName: MockRecord.entityName) + let result = wrapper.fetch(withRequest: request) + + XCTAssertEqual(result.count, 0) + } + + func test_deleteRecords() throws { + // given a wrapper with data + let record1 = MockRecord.create(context: wrapper.context, id: "test1") + let record2 = MockRecord.create(context: wrapper.context, id: "test2") + wrapper.save() + + // when deleting the record + wrapper.deleteRecords([record1, record2]) + + // then the record is deleted + let request = NSFetchRequest(entityName: MockRecord.entityName) + let result = wrapper.fetch(withRequest: request) + + XCTAssertEqual(result.count, 0) + } +} + +class MockRecord: NSManagedObject { + @NSManaged var id: String + + class func create(context: NSManagedObjectContext, id: String) -> MockRecord { + let record = MockRecord(context: context) + record.id = id + return record + } + + static let entityName = "MockRecord" + + static var entityDescription: NSEntityDescription { + let entity = NSEntityDescription() + entity.name = entityName + entity.managedObjectClassName = NSStringFromClass(MockRecord.self) + + let idAttribute = NSAttributeDescription() + idAttribute.name = "id" + idAttribute.attributeType = .stringAttributeType + + entity.properties = [idAttribute] + return entity + } +} diff --git a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift index 97849062..4b6e3dc8 100644 --- a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift @@ -7,6 +7,7 @@ import XCTest import EmbraceStorageInternal import EmbraceCommonInternal import TestSupport +import CoreData // swiftlint:disable force_cast @@ -327,6 +328,31 @@ final class MetadataHandlerTests: XCTestCase { XCTAssertNil(result) } + // MARK: tmp core data + func test_coreDataClone() throws { + // given stored metadata + for i in 1...3 { + try storage.addMetadata(key: "resource\(i)", value: "test", type: .resource, lifespan: .permanent) + try storage.addMetadata(key: "property\(i)", value: "test", type: .customProperty, lifespan: .permanent) + } + + // when initializing a metadata handler + let handler = MetadataHandler(storage: storage, sessionController: sessionController) + + // the data is cloned into a temporal core data stack + XCTAssertNotNil(handler.coreData) + + let request = NSFetchRequest(entityName: MetadataRecordTmp.entityName) + let result = handler.coreData!.fetch(withRequest: request) + + XCTAssertEqual(result.count, 6) + XCTAssertNotNil(result.first(where: { $0.key == "resource1" })) + XCTAssertNotNil(result.first(where: { $0.key == "resource2" })) + XCTAssertNotNil(result.first(where: { $0.key == "resource3" })) + XCTAssertNotNil(result.first(where: { $0.key == "property1" })) + XCTAssertNotNil(result.first(where: { $0.key == "property2" })) + XCTAssertNotNil(result.first(where: { $0.key == "property3" })) + } } // swiftlint:enable force_cast diff --git a/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift b/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift index c6833ed1..201b6fec 100644 --- a/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift @@ -307,7 +307,7 @@ class MetadataRecordTests: XCTestCase { try storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) // when updating its value - try storage.updateMetadata(key: "test", value: "value", type: .resource, lifespan: .permanent) + try storage.updateMetadata(key: "test", value: "value", type: .resource, lifespan: .permanent, lifespanId: "") // then record should exist in storage with the correct value let expectation = XCTestExpectation() From 02a0f28f0b03f11608d7c2fd1512dc98df849ce3 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Wed, 22 Jan 2025 15:09:20 -0300 Subject: [PATCH 06/17] Clean up --- Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index 41301b29..047b8d27 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -56,7 +56,8 @@ public class CoreDataWrapper { do { try self?.context.save() } catch { - self?.logger.warning("Erro saving EmbraceUploadCache: \(error.localizedDescription)") + let name = self?.context.name ?? "???" + self?.logger.warning("Error saving CoreData \"\(name)\": \(error.localizedDescription)") } } } From 83701180a021ca7ec4ef5c42bddd61eb4b27005c Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman <114942102+NachoEmbrace@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:30:18 -0300 Subject: [PATCH 07/17] Update Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift Co-authored-by: ArielDemarco --- Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index 047b8d27..8370c37c 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -50,7 +50,7 @@ public class CoreDataWrapper { self.context.persistentStoreCoordinator = self.container.persistentStoreCoordinator } - /// Synchronously saves all changes on the current context to disk + /// Asynchronously saves all changes on the current context to disk public func save() { context.perform { [weak self] in do { From a97b6027f1889183709e0b5235e1c5dcd486df02 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Mon, 27 Jan 2025 13:42:43 -0300 Subject: [PATCH 08/17] WIP --- .../EmbraceCore/Internal/Embrace+Setup.swift | 4 +- .../CoreDataWrapper.swift | 15 +- ...sIdentifier+DatabaseValueConvertible.swift | 20 - ...nIdentifier+DatabaseValueConvertible.swift | 20 - .../SpanType+DatabaseValueConvertible.swift | 20 - .../EmbraceStorage+AsyncUtils.swift | 79 --- .../EmbraceStorage+Options.swift | 52 +- .../EmbraceStorage.swift | 195 +------ .../Migration/Migration.swift | 50 +- .../Migration/MigrationService.swift | 86 +-- .../20240509_00_AddSpanRecordMigration.swift | 70 +-- ...0240510_00_AddSessionRecordMigration.swift | 76 +-- ...240510_01_AddMetadataRecordMigration.swift | 54 +- .../20240510_02_AddLogRecordMigration.swift | 38 +- ...ocessIdentifierToSpanRecordMigration.swift | 142 ++--- .../Migrations/Migrations+Current.swift | 36 +- .../Queries/SpanRecord+SessionQuery.swift | 61 --- .../Records/EmbraceStorage+Metadata.swift | 501 ++++++++++-------- .../Records/EmbraceStorage+Session.swift | 79 +-- .../Records/EmbraceStorage+Span.swift | 244 ++++----- .../Records/EmbraceStorageRecord.swift | 11 + .../Records/MetadataRecord+ValueTypes.swift | 55 -- .../Records/MetadataRecord.swift | 107 ++-- .../Records/SessionRecord.swift | 168 ++++-- .../Records/SpanRecord.swift | 132 +++-- .../EmbraceStorageTests+Async.swift | 198 ------- .../EmbraceStorageTests.swift | 37 -- 27 files changed, 1015 insertions(+), 1535 deletions(-) delete mode 100644 Sources/EmbraceStorageInternal/DatabaseValue/ProcessIdentifier+DatabaseValueConvertible.swift delete mode 100644 Sources/EmbraceStorageInternal/DatabaseValue/SessionIdentifier+DatabaseValueConvertible.swift delete mode 100644 Sources/EmbraceStorageInternal/DatabaseValue/SpanType+DatabaseValueConvertible.swift delete mode 100644 Sources/EmbraceStorageInternal/EmbraceStorage+AsyncUtils.swift delete mode 100644 Sources/EmbraceStorageInternal/Queries/SpanRecord+SessionQuery.swift create mode 100644 Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift delete mode 100644 Sources/EmbraceStorageInternal/Records/MetadataRecord+ValueTypes.swift delete mode 100644 Tests/EmbraceStorageInternalTests/EmbraceStorageTests+Async.swift diff --git a/Sources/EmbraceCore/Internal/Embrace+Setup.swift b/Sources/EmbraceCore/Internal/Embrace+Setup.swift index f4e61e8a..53ea6bf7 100644 --- a/Sources/EmbraceCore/Internal/Embrace+Setup.swift +++ b/Sources/EmbraceCore/Internal/Embrace+Setup.swift @@ -19,9 +19,9 @@ extension Embrace { partitionId: partitionId, appGroupId: options.appGroupId ) { - let storageOptions = EmbraceStorage.Options(baseUrl: storageUrl, fileName: "db.sqlite") + let storageMechanism: StorageMechanism = .onDisk(name: "EmbraceStorage", baseURL: storageUrl) + let storageOptions = EmbraceStorage.Options(storageMechanism: storageMechanism) let storage = try EmbraceStorage(options: storageOptions, logger: Embrace.logger) - try storage.performMigration() return storage } else { throw EmbraceSetupError.failedStorageCreation(partitionId: partitionId, appGroupId: options.appGroupId) diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index 8370c37c..62925613 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -73,12 +73,23 @@ public class CoreDataWrapper { return result } - /// Asynchronously deletes record from the database + /// Synchronously fetches the count of records that satisfy the given request + public func count(withRequest request: NSFetchRequest) -> Int where T: NSManagedObject { + var result: Int = 0 + context.performAndWait { + do { + result = try context.count(for: request) + } catch { } + } + return result + } + + /// Asynchronously deletes record from the database and saves public func deleteRecord(_ record: T) where T: NSManagedObject { deleteRecords([record]) } - /// Asynchronously deletes requested records from the database + /// Asynchronously deletes requested records from the database and saves public func deleteRecords(_ records: [T]) where T: NSManagedObject { context.perform { [weak self] in for record in records { diff --git a/Sources/EmbraceStorageInternal/DatabaseValue/ProcessIdentifier+DatabaseValueConvertible.swift b/Sources/EmbraceStorageInternal/DatabaseValue/ProcessIdentifier+DatabaseValueConvertible.swift deleted file mode 100644 index 9341dc0a..00000000 --- a/Sources/EmbraceStorageInternal/DatabaseValue/ProcessIdentifier+DatabaseValueConvertible.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import EmbraceCommonInternal -import GRDB - -extension ProcessIdentifier: DatabaseValueConvertible { - public var databaseValue: DatabaseValue { - return String(hex).databaseValue - } - - public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> ProcessIdentifier? { - guard let hex = String.fromDatabaseValue(dbValue) else { - return nil - } - - return ProcessIdentifier(hex: hex) - } -} diff --git a/Sources/EmbraceStorageInternal/DatabaseValue/SessionIdentifier+DatabaseValueConvertible.swift b/Sources/EmbraceStorageInternal/DatabaseValue/SessionIdentifier+DatabaseValueConvertible.swift deleted file mode 100644 index 5c0e0928..00000000 --- a/Sources/EmbraceStorageInternal/DatabaseValue/SessionIdentifier+DatabaseValueConvertible.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import EmbraceCommonInternal -import GRDB - -extension SessionIdentifier: DatabaseValueConvertible { - public var databaseValue: DatabaseValue { - return toString.databaseValue - } - - public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> SessionIdentifier? { - guard let uuidString = String.fromDatabaseValue(dbValue) else { - return nil - } - - return SessionIdentifier(string: uuidString) - } -} diff --git a/Sources/EmbraceStorageInternal/DatabaseValue/SpanType+DatabaseValueConvertible.swift b/Sources/EmbraceStorageInternal/DatabaseValue/SpanType+DatabaseValueConvertible.swift deleted file mode 100644 index 427cc17d..00000000 --- a/Sources/EmbraceStorageInternal/DatabaseValue/SpanType+DatabaseValueConvertible.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import GRDB -import EmbraceCommonInternal - -extension SpanType: DatabaseValueConvertible { - public var databaseValue: DatabaseValue { - return String(rawValue).databaseValue - } - - public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> SpanType? { - guard let rawValue = String.fromDatabaseValue(dbValue) else { - return nil - } - - return SpanType(rawValue: rawValue) - } -} diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage+AsyncUtils.swift b/Sources/EmbraceStorageInternal/EmbraceStorage+AsyncUtils.swift deleted file mode 100644 index c846936e..00000000 --- a/Sources/EmbraceStorageInternal/EmbraceStorage+AsyncUtils.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import GRDB - -extension EmbraceStorage { - internal func dbFetchAsync( - block: @escaping (Database) throws -> [T], - completion: @escaping (Result<[T], Error>) -> Void) { - - dbQueue.asyncRead { result in - switch result { - case .success(let db): - do { - let fetch = try block(db) - completion(.success(fetch)) - } catch { - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(error)) - } - } - } - - internal func dbFetchOneAsync( - block: @escaping (Database) throws -> T?, - completion: @escaping (Result) -> Void) { - - dbQueue.asyncRead { result in - switch result { - case .success(let db): - do { - let fetch = try block(db) - completion(.success(fetch)) - } catch { - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(error)) - } - } - } - - internal func dbFetchCountAsync( - block: @escaping (Database) throws -> Int, - completion: @escaping (Result) -> Void) { - - dbQueue.asyncRead { result in - switch result { - case .success(let db): - do { - let fetch = try block(db) - completion(.success(fetch)) - } catch { - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(error)) - } - } - } - - internal func dbWriteAsync( - block: @escaping (Database) throws -> T, - completion: ((Result) -> Void)?) { - - dbQueue.asyncWrite { db in - try block(db) - } completion: { _, result in - completion?(result) - } - } -} diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift b/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift index c3f58b1e..243c3cfb 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage+Options.swift @@ -6,10 +6,6 @@ import Foundation import EmbraceCommonInternal public extension EmbraceStorage { - enum StorageMechanism { - case inMemory(name: String) - case onDisk(baseURL: URL, fileName: String) - } /// Class used to configure a EmbraceStorage instance class Options { @@ -30,51 +26,9 @@ public extension EmbraceStorage { /// Use this initializer to create a storage object that is persisted locally to disk /// - Parameters: - /// - baseUrl: The URL to the directory this storage object should use to persist data. Must be a URL to a local directory. - /// - fileName: The filename that will be used for the file of this storage object on disk. - public init(baseUrl: URL, fileName: String) { - precondition(baseUrl.isFileURL, "baseURL must be a fileURL") - storageMechanism = .onDisk(baseURL: baseUrl, fileName: fileName) - } - - /// Use this initializer to create an inMemory storage - /// - Parameter name: The name of the underlying storage object - public init(named name: String) { - storageMechanism = .inMemory(name: name) - } - } -} - -extension EmbraceStorage.Options { - /// The name of the storage item when using an inMemory storage - public var name: String? { - if case let .inMemory(name) = storageMechanism { - return name - } - return nil - } - - /// URL pointing to the folder where the storage will be saved - public var baseUrl: URL? { - if case let .onDisk(baseURL, _) = storageMechanism { - return baseURL - } - return nil - } - - /// URL pointing to the folder where the storage will be saved - public var fileName: String? { - if case let .onDisk(_, name) = storageMechanism { - return name - } - return nil - } - - /// URL to the storage file - public var fileURL: URL? { - if case let .onDisk(url, filename) = storageMechanism { - return url.appendingPathComponent(filename) + /// - storageMechanism: The StorageMechanism to use + public init(storageMechanism: StorageMechanism) { + self.storageMechanism = storageMechanism } - return nil } } diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage.swift b/Sources/EmbraceStorageInternal/EmbraceStorage.swift index 7883b67a..e65f5dd6 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage.swift @@ -4,6 +4,8 @@ import Foundation import EmbraceCommonInternal +import EmbraceCoreDataInternal +import CoreData import GRDB public typealias Storage = EmbraceStorageMetadataFetcher & LogRepository @@ -12,8 +14,8 @@ public typealias Storage = EmbraceStorageMetadataFetcher & LogRepository /// It provides an abstraction layer over a GRDB SQLite database. public class EmbraceStorage: Storage { public private(set) var options: Options - public private(set) var dbQueue: DatabaseQueue public private(set) var logger: InternalLogger + public private(set) var coreData: CoreDataWrapper /// Returns an `EmbraceStorage` instance for the given `EmbraceStorage.Options` /// - Parameters: @@ -22,186 +24,43 @@ public class EmbraceStorage: Storage { public init(options: Options, logger: InternalLogger) throws { self.options = options self.logger = logger - dbQueue = try Self.createDBQueue(options: options, logger: logger) - } - /// Performs any DB migrations - /// - Parameters: - /// - resetIfError: If true and the migrations fail the DB will be reset entirely. - public func performMigration( - resetIfError: Bool = true, - migrations: [Migration] = .current - ) throws { - do { - try MigrationService(logger: logger).perform(dbQueue, migrations: migrations) - } catch let error { - if resetIfError { - logger.error("Error performing migrations, resetting EmbraceStorage: \(error)") - try reset(migrations: migrations) - } else { - logger.error("Error performing migrations. Reset not enabled: \(error)") - throw error // re-throw error if auto-recover is not enabled - } + // remove old GRDB sqlite file + if let url = options.storageMechanism.baseUrl?.appendingPathComponent("db.sqlite") { + try? FileManager.default.removeItem(at: url) } - } - /// Deletes the database and recreates it from scratch - func reset(migrations: [Migration] = .current) throws { - if let fileURL = options.fileURL { - try FileManager.default.removeItem(at: fileURL) - } + // create core data stack + let coreDataOptions = CoreDataWrapper.Options( + storageMechanism: options.storageMechanism, + entities: [ + SessionRecord.entityDescription, + SpanRecord.entityDescription, + MetadataRecord.entityDescription, + LogRecord.entityDescriptio + ] + ) + self.coreData = try CoreDataWrapper(options: coreDataOptions, logger: logger) + } - dbQueue = try Self.createDBQueue(options: options, logger: logger) - try performMigration(resetIfError: false, migrations: migrations) // Do not perpetuate loop + /// Saves all changes to disk + public func save() { + coreData.save() } } // MARK: - Sync operations extension EmbraceStorage { - /// Updates a record in the storage synchronously. - /// - Parameter record: `PersistableRecord` to update - public func update(record: PersistableRecord) throws { - try dbQueue.write { db in - try record.update(db) - } - } - /// Deletes a record from the storage synchronously. - /// - Parameter record: `PersistableRecord` to delete - /// - Returns: Boolean indicating if the record was successfully deleted - @discardableResult public func delete(record: PersistableRecord) throws -> Bool { - try dbQueue.write { db in - return try record.delete(db) - } + /// - Parameter record: `NSManagedObject` to delete + public func delete(_ record: T) throws { + coreData.deleteRecord(record) } /// Fetches all the records of the given type in the storage synchronously. /// - Returns: Array containing all the records of the given type - public func fetchAll() throws -> [T] { - try dbQueue.read { db in - return try T.fetchAll(db) - } - } - - /// Executes the given SQL query synchronously. - /// - Parameters: - /// - sql: SQL query to execute - /// - arguments: Arguments for the query, if any - public func executeQuery(_ sql: String, arguments: StatementArguments?) throws { - try dbQueue.write { db in - try db.execute(sql: sql, arguments: arguments ?? StatementArguments()) - } - } -} - -// MARK: - Async operations -extension EmbraceStorage { - /// Updates a record in the storage asynchronously. - /// - Parameters: - /// - record: `PersistableRecord` to update - /// - completion: Completion block called with an `Error` on failure - public func updateAsync(record: PersistableRecord, completion: ((Result<(), Error>) -> Void)?) { - dbWriteAsync(block: { db in - try record.update(db) - }, completion: completion) - } - - /// Deletes a record from the storage asynchronously. - /// - Parameters: - /// - record: `PersistableRecord` to delete - /// - completion: Completion block called with an `Error` on failure - /// - Returns: Boolean indicating if the record was successfully deleted - public func deleteAsync(record: PersistableRecord, completion: ((Result) -> Void)?) { - dbWriteAsync(block: { db in - try record.delete(db) - }, completion: completion) - } - - /// Fetches all the records of the given type in the storage asynchronously. - /// - Parameter completion: Completion block called with an array `[T]` with the fetch result on success, or an `Error` on failure - /// - Returns: Array containing all the records of the given type - public func fetchAllAsync(completion: @escaping (Result<[T], Error>) -> Void) { - dbFetchAsync(block: { db in - return try T.fetchAll(db) - }, completion: completion) - } - - /// Executes the given SQL query asynchronously. - /// - Parameters: - /// - sql: SQL query to execute - /// - arguments: Arguments for the query, if any - /// - completion: Completion block called with an `Error` on failure - public func executeQueryAsync( - _ sql: String, - arguments: StatementArguments?, - completion: ((Result) -> Void)? - ) { - dbWriteAsync(block: { db in - try db.execute(sql: sql, arguments: arguments ?? StatementArguments()) - }, completion: completion) - } -} - -extension EmbraceStorage { - - private static func createDBQueue( - options: EmbraceStorage.Options, - logger: InternalLogger - ) throws -> DatabaseQueue { - if case let .inMemory(name) = options.storageMechanism { - return try DatabaseQueue(named: name) - } else if case let .onDisk(baseURL, _) = options.storageMechanism, let fileURL = options.fileURL { - // create base directory if necessary - try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) - return try EmbraceStorage.getDBQueueIfPossible(at: fileURL, logger: logger) - } else { - fatalError("Unsupported storage mechanism added") - } - } - - /// Will attempt to create or open the DB File. If first attempt fails due to GRDB error, it'll assume the existing DB is corruped and try again after deleting the existing DB file. - private static func getDBQueueIfPossible(at fileURL: URL, logger: InternalLogger) throws -> DatabaseQueue { - do { - return try DatabaseQueue(path: fileURL.path) - } catch { - if let dbError = error as? DatabaseError { - logger.error( - """ - GRDB Failed to initialize EmbraceStorage. - Will attempt to remove existing file and create a new DB. - Message: \(dbError.message ?? "[empty message]"), - Result Code: \(dbError.resultCode), - SQLite Extended Code: \(dbError.extendedResultCode) - """ - ) - } else { - logger.error( - """ - Unknown error while trying to initialize EmbraceStorage: \(error) - Will attempt to recover by deleting existing DB. - """ - ) - } - } - - try EmbraceStorage.deleteDBFile(at: fileURL, logger: logger) - - return try DatabaseQueue(path: fileURL.path) - } - - /// Will attempt to delete the provided file. - private static func deleteDBFile(at fileURL: URL, logger: InternalLogger) throws { - do { - let fileURL = URL(fileURLWithPath: fileURL.path) - try FileManager.default.removeItem(at: fileURL) - } catch let error { - logger.error( - """ - EmbraceStorage failed to remove DB file. - Error: \(error.localizedDescription) - Filepath: \(fileURL) - """ - ) - } + public func fetchAll() throws -> [T] { + let request = NSFetchRequest(entityName: T.entityName) + return coreData.fetch(withRequest: request) } } diff --git a/Sources/EmbraceStorageInternal/Migration/Migration.swift b/Sources/EmbraceStorageInternal/Migration/Migration.swift index 5777c63a..011276a0 100644 --- a/Sources/EmbraceStorageInternal/Migration/Migration.swift +++ b/Sources/EmbraceStorageInternal/Migration/Migration.swift @@ -1,27 +1,27 @@ +//// +//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +//// // -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// import Foundation +// import GRDB // - -import Foundation -import GRDB - -public protocol Migration { - /// The identifier to register this migration under. Must be unique - static var identifier: StringLiteralType { get } - - /// Controls how this migration handles foreign key constraints. Defaults to `immediate`. - static var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { get } - - /// Operation that performs migration. - /// See [GRDB Reference](https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/migrations). - func perform(_ db: Database) throws -} - -extension Migration { - var identifier: StringLiteralType { Self.identifier } - var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { Self.foreignKeyChecks } -} - -extension Migration { - public static var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { .immediate } -} +// public protocol Migration { +// /// The identifier to register this migration under. Must be unique +// static var identifier: StringLiteralType { get } +// +// /// Controls how this migration handles foreign key constraints. Defaults to `immediate`. +// static var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { get } +// +// /// Operation that performs migration. +// /// See [GRDB Reference](https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/migrations). +// func perform(_ db: Database) throws +// } +// +// extension Migration { +// var identifier: StringLiteralType { Self.identifier } +// var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { Self.foreignKeyChecks } +// } +// +// extension Migration { +// public static var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { .immediate } +// } diff --git a/Sources/EmbraceStorageInternal/Migration/MigrationService.swift b/Sources/EmbraceStorageInternal/Migration/MigrationService.swift index 9672f743..eacf23fa 100644 --- a/Sources/EmbraceStorageInternal/Migration/MigrationService.swift +++ b/Sources/EmbraceStorageInternal/Migration/MigrationService.swift @@ -1,44 +1,44 @@ +//// +//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +//// // -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import GRDB -import EmbraceCommonInternal - -public protocol MigrationServiceProtocol { - func perform(_ dbQueue: DatabaseWriter, migrations: [Migration]) throws -} - -final public class MigrationService: MigrationServiceProtocol { - - let logger: InternalLogger - - public init(logger: InternalLogger) { - self.logger = logger - } - - public func perform(_ dbQueue: DatabaseWriter, migrations: [Migration]) throws { - guard migrations.count > 0 else { - logger.debug("No migrations to perform") - return - } - - var migrator = DatabaseMigrator() - migrations.forEach { migration in - migrator.registerMigration(migration.identifier, - foreignKeyChecks: migration.foreignKeyChecks, - migrate: migration.perform(_:)) - } - - try dbQueue.read { db in - if try migrator.hasCompletedMigrations(db) { - logger.debug("DB is up to date") - return - } else { - logger.debug("Running up to \(migrations.count) migrations") - } - } - - try migrator.migrate(dbQueue) - } -} +// import GRDB +// import EmbraceCommonInternal +// +// public protocol MigrationServiceProtocol { +// func perform(_ dbQueue: DatabaseWriter, migrations: [Migration]) throws +// } +// +// final public class MigrationService: MigrationServiceProtocol { +// +// let logger: InternalLogger +// +// public init(logger: InternalLogger) { +// self.logger = logger +// } +// +// public func perform(_ dbQueue: DatabaseWriter, migrations: [Migration]) throws { +// guard migrations.count > 0 else { +// logger.debug("No migrations to perform") +// return +// } +// +// var migrator = DatabaseMigrator() +// migrations.forEach { migration in +// migrator.registerMigration(migration.identifier, +// foreignKeyChecks: migration.foreignKeyChecks, +// migrate: migration.perform(_:)) +// } +// +// try dbQueue.read { db in +// if try migrator.hasCompletedMigrations(db) { +// logger.debug("DB is up to date") +// return +// } else { +// logger.debug("Running up to \(migrations.count) migrations") +// } +// } +// +// try migrator.migrate(dbQueue) +// } +// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240509_00_AddSpanRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240509_00_AddSpanRecordMigration.swift index e0b6fb98..2a0527ea 100644 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240509_00_AddSpanRecordMigration.swift +++ b/Sources/EmbraceStorageInternal/Migration/Migrations/20240509_00_AddSpanRecordMigration.swift @@ -1,36 +1,36 @@ +//// +//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +//// // -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import GRDB - -struct AddSpanRecordMigration: Migration { - static let identifier = "CreateSpanRecordTable" // DEV: Must not change - - func perform(_ db: GRDB.Database) throws { - try db.create(table: SpanRecord.databaseTableName, options: .ifNotExists) { t in - t.column(SpanRecord.Schema.id.name, .text).notNull() - t.column(SpanRecord.Schema.name.name, .text).notNull() - t.column(SpanRecord.Schema.traceId.name, .text).notNull() - t.primaryKey([SpanRecord.Schema.traceId.name, SpanRecord.Schema.id.name]) - - t.column(SpanRecord.Schema.type.name, .text).notNull() - t.column(SpanRecord.Schema.startTime.name, .datetime).notNull() - t.column(SpanRecord.Schema.endTime.name, .datetime) - - t.column(SpanRecord.Schema.data.name, .blob).notNull() - } - - let preventClosedSpanModification = """ - CREATE TRIGGER IF NOT EXISTS prevent_closed_span_modification - BEFORE UPDATE ON \(SpanRecord.databaseTableName) - WHEN OLD.end_time IS NOT NULL - BEGIN - SELECT RAISE(ABORT,'Attempted to modify an already closed span.'); - END; - """ - - try db.execute(sql: preventClosedSpanModification) - } - -} +// import GRDB +// +// struct AddSpanRecordMigration: Migration { +// static let identifier = "CreateSpanRecordTable" // DEV: Must not change +// +// func perform(_ db: GRDB.Database) throws { +// try db.create(table: SpanRecord.databaseTableName, options: .ifNotExists) { t in +// t.column(SpanRecord.Schema.id.name, .text).notNull() +// t.column(SpanRecord.Schema.name.name, .text).notNull() +// t.column(SpanRecord.Schema.traceId.name, .text).notNull() +// t.primaryKey([SpanRecord.Schema.traceId.name, SpanRecord.Schema.id.name]) +// +// t.column(SpanRecord.Schema.type.name, .text).notNull() +// t.column(SpanRecord.Schema.startTime.name, .datetime).notNull() +// t.column(SpanRecord.Schema.endTime.name, .datetime) +// +// t.column(SpanRecord.Schema.data.name, .blob).notNull() +// } +// +// let preventClosedSpanModification = """ +// CREATE TRIGGER IF NOT EXISTS prevent_closed_span_modification +// BEFORE UPDATE ON \(SpanRecord.databaseTableName) +// WHEN OLD.end_time IS NOT NULL +// BEGIN +// SELECT RAISE(ABORT,'Attempted to modify an already closed span.'); +// END; +// """ +// +// try db.execute(sql: preventClosedSpanModification) +// } +// +// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_00_AddSessionRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_00_AddSessionRecordMigration.swift index ad55b0ec..dbb9e690 100644 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_00_AddSessionRecordMigration.swift +++ b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_00_AddSessionRecordMigration.swift @@ -1,39 +1,39 @@ +//// +//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +//// // -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import GRDB - -struct AddSessionRecordMigration: Migration { - static var identifier = "CreateSessionRecordTable" // DEV: Must not change - - func perform(_ db: GRDB.Database) throws { - try db.create(table: SessionRecord.databaseTableName, options: .ifNotExists) { t in - - t.primaryKey(SessionRecord.Schema.id.name, .text).notNull() - - t.column(SessionRecord.Schema.state.name, .text).notNull() - t.column(SessionRecord.Schema.processId.name, .text).notNull() - t.column(SessionRecord.Schema.traceId.name, .text).notNull() - t.column(SessionRecord.Schema.spanId.name, .text).notNull() - - t.column(SessionRecord.Schema.startTime.name, .datetime).notNull() - t.column(SessionRecord.Schema.endTime.name, .datetime) - t.column(SessionRecord.Schema.lastHeartbeatTime.name, .datetime).notNull() - - t.column(SessionRecord.Schema.coldStart.name, .boolean) - .notNull() - .defaults(to: false) - - t.column(SessionRecord.Schema.cleanExit.name, .boolean) - .notNull() - .defaults(to: false) - - t.column(SessionRecord.Schema.appTerminated.name, .boolean) - .notNull() - .defaults(to: false) - - t.column(SessionRecord.Schema.crashReportId.name, .text) - } - } -} +// import GRDB +// +// struct AddSessionRecordMigration: Migration { +// static var identifier = "CreateSessionRecordTable" // DEV: Must not change +// +// func perform(_ db: GRDB.Database) throws { +// try db.create(table: SessionRecord.databaseTableName, options: .ifNotExists) { t in +// +// t.primaryKey(SessionRecord.Schema.id.name, .text).notNull() +// +// t.column(SessionRecord.Schema.state.name, .text).notNull() +// t.column(SessionRecord.Schema.processId.name, .text).notNull() +// t.column(SessionRecord.Schema.traceId.name, .text).notNull() +// t.column(SessionRecord.Schema.spanId.name, .text).notNull() +// +// t.column(SessionRecord.Schema.startTime.name, .datetime).notNull() +// t.column(SessionRecord.Schema.endTime.name, .datetime) +// t.column(SessionRecord.Schema.lastHeartbeatTime.name, .datetime).notNull() +// +// t.column(SessionRecord.Schema.coldStart.name, .boolean) +// .notNull() +// .defaults(to: false) +// +// t.column(SessionRecord.Schema.cleanExit.name, .boolean) +// .notNull() +// .defaults(to: false) +// +// t.column(SessionRecord.Schema.appTerminated.name, .boolean) +// .notNull() +// .defaults(to: false) +// +// t.column(SessionRecord.Schema.crashReportId.name, .text) +// } +// } +// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_01_AddMetadataRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_01_AddMetadataRecordMigration.swift index 78890f30..6a7b0e64 100644 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_01_AddMetadataRecordMigration.swift +++ b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_01_AddMetadataRecordMigration.swift @@ -1,29 +1,29 @@ +//// +//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +//// // -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// import GRDB // - -import GRDB - -struct AddMetadataRecordMigration: Migration { - - static var identifier = "CreateMetadataRecordTable" // DEV: Must not change - - func perform(_ db: Database) throws { - try db.create(table: MetadataRecord.databaseTableName, options: .ifNotExists) { t in - - t.column(MetadataRecord.Schema.key.name, .text).notNull() - t.column(MetadataRecord.Schema.value.name, .text).notNull() - t.column(MetadataRecord.Schema.type.name, .text).notNull() - t.column(MetadataRecord.Schema.lifespan.name, .text).notNull() - t.column(MetadataRecord.Schema.lifespanId.name, .text).notNull() - t.column(MetadataRecord.Schema.collectedAt.name, .datetime).notNull() - - t.primaryKey([ - MetadataRecord.Schema.key.name, - MetadataRecord.Schema.type.name, - MetadataRecord.Schema.lifespan.name, - MetadataRecord.Schema.lifespanId.name - ]) - } - } -} +// struct AddMetadataRecordMigration: Migration { +// +// static var identifier = "CreateMetadataRecordTable" // DEV: Must not change +// +// func perform(_ db: Database) throws { +// try db.create(table: MetadataRecord.databaseTableName, options: .ifNotExists) { t in +// +// t.column(MetadataRecord.Schema.key.name, .text).notNull() +// t.column(MetadataRecord.Schema.value.name, .text).notNull() +// t.column(MetadataRecord.Schema.type.name, .text).notNull() +// t.column(MetadataRecord.Schema.lifespan.name, .text).notNull() +// t.column(MetadataRecord.Schema.lifespanId.name, .text).notNull() +// t.column(MetadataRecord.Schema.collectedAt.name, .datetime).notNull() +// +// t.primaryKey([ +// MetadataRecord.Schema.key.name, +// MetadataRecord.Schema.type.name, +// MetadataRecord.Schema.lifespan.name, +// MetadataRecord.Schema.lifespanId.name +// ]) +// } +// } +// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_02_AddLogRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_02_AddLogRecordMigration.swift index 9ee1887d..95710fd5 100644 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_02_AddLogRecordMigration.swift +++ b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_02_AddLogRecordMigration.swift @@ -1,21 +1,21 @@ +//// +//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +//// // -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// import GRDB // - -import GRDB - -struct AddLogRecordMigration: Migration { - - static var identifier = "CreateLogRecordTable" // DEV: Must not change - - func perform(_ db: Database) throws { - try db.create(table: LogRecord.databaseTableName, options: .ifNotExists) { t in - t.primaryKey(LogRecord.Schema.identifier.name, .text).notNull() - t.column(LogRecord.Schema.processIdentifier.name, .integer).notNull() - t.column(LogRecord.Schema.severity.name, .integer).notNull() - t.column(LogRecord.Schema.body.name, .text).notNull() - t.column(LogRecord.Schema.timestamp.name, .datetime).notNull() - t.column(LogRecord.Schema.attributes.name, .text).notNull() - } - } -} +// struct AddLogRecordMigration: Migration { +// +// static var identifier = "CreateLogRecordTable" // DEV: Must not change +// +// func perform(_ db: Database) throws { +// try db.create(table: LogRecord.databaseTableName, options: .ifNotExists) { t in +// t.primaryKey(LogRecord.Schema.identifier.name, .text).notNull() +// t.column(LogRecord.Schema.processIdentifier.name, .integer).notNull() +// t.column(LogRecord.Schema.severity.name, .integer).notNull() +// t.column(LogRecord.Schema.body.name, .text).notNull() +// t.column(LogRecord.Schema.timestamp.name, .datetime).notNull() +// t.column(LogRecord.Schema.attributes.name, .text).notNull() +// } +// } +// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigration.swift index 54668db9..0c71885a 100644 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigration.swift +++ b/Sources/EmbraceStorageInternal/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigration.swift @@ -1,73 +1,73 @@ +//// +//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +//// // -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// import GRDB // - -import GRDB - -struct AddProcessIdentifierToSpanRecordMigration: Migration { - static var identifier = "AddProcessIdentifierToSpanRecord" // DEV: Must not change - - private static var tempSpansTableName = "spans_temp" - - func perform(_ db: GRDB.Database) throws { - - // create copy of `spans` table in `spans_temp` - // include new column 'process_identifier' - try db.create(table: Self.tempSpansTableName) { t in - - t.column(SpanRecord.Schema.id.name, .text).notNull() - t.column(SpanRecord.Schema.traceId.name, .text).notNull() - t.primaryKey([SpanRecord.Schema.traceId.name, SpanRecord.Schema.id.name]) - - t.column(SpanRecord.Schema.name.name, .text).notNull() - t.column(SpanRecord.Schema.type.name, .text).notNull() - t.column(SpanRecord.Schema.startTime.name, .datetime).notNull() - t.column(SpanRecord.Schema.endTime.name, .datetime) - t.column(SpanRecord.Schema.data.name, .blob).notNull() - - // include new column into `spans_temp` table - t.column(SpanRecord.Schema.processIdentifier.name, .text).notNull() - } - - // copy all existing data into temp table - // include default value for `process_identifier` - try db.execute(literal: """ - INSERT INTO 'spans_temp' ( - 'id', - 'trace_id', - 'name', - 'type', - 'start_time', - 'end_time', - 'data', - 'process_identifier' - ) SELECT - id, - trace_id, - name, - type, - start_time, - end_time, - data, - 'c0ffee' - FROM 'spans' - """) - - // drop original table - try db.drop(table: SpanRecord.databaseTableName) - - // rename temp table to be original table - try db.rename(table: Self.tempSpansTableName, to: SpanRecord.databaseTableName) - - // Create Trigger on new spans table to prevent endTime from being modified on SpanRecord - try db.execute(sql: - """ - CREATE TRIGGER IF NOT EXISTS prevent_closed_span_modification - BEFORE UPDATE ON \(SpanRecord.databaseTableName) - WHEN OLD.end_time IS NOT NULL - BEGIN - SELECT RAISE(ABORT,'Attempted to modify an already closed span.'); - END; - """ ) - } -} +// struct AddProcessIdentifierToSpanRecordMigration: Migration { +// static var identifier = "AddProcessIdentifierToSpanRecord" // DEV: Must not change +// +// private static var tempSpansTableName = "spans_temp" +// +// func perform(_ db: GRDB.Database) throws { +// +// // create copy of `spans` table in `spans_temp` +// // include new column 'process_identifier' +// try db.create(table: Self.tempSpansTableName) { t in +// +// t.column(SpanRecord.Schema.id.name, .text).notNull() +// t.column(SpanRecord.Schema.traceId.name, .text).notNull() +// t.primaryKey([SpanRecord.Schema.traceId.name, SpanRecord.Schema.id.name]) +// +// t.column(SpanRecord.Schema.name.name, .text).notNull() +// t.column(SpanRecord.Schema.type.name, .text).notNull() +// t.column(SpanRecord.Schema.startTime.name, .datetime).notNull() +// t.column(SpanRecord.Schema.endTime.name, .datetime) +// t.column(SpanRecord.Schema.data.name, .blob).notNull() +// +// // include new column into `spans_temp` table +// t.column(SpanRecord.Schema.processIdentifier.name, .text).notNull() +// } +// +// // copy all existing data into temp table +// // include default value for `process_identifier` +// try db.execute(literal: """ +// INSERT INTO 'spans_temp' ( +// 'id', +// 'trace_id', +// 'name', +// 'type', +// 'start_time', +// 'end_time', +// 'data', +// 'process_identifier' +// ) SELECT +// id, +// trace_id, +// name, +// type, +// start_time, +// end_time, +// data, +// 'c0ffee' +// FROM 'spans' +// """) +// +// // drop original table +// try db.drop(table: SpanRecord.databaseTableName) +// +// // rename temp table to be original table +// try db.rename(table: Self.tempSpansTableName, to: SpanRecord.databaseTableName) +// +// // Create Trigger on new spans table to prevent endTime from being modified on SpanRecord +// try db.execute(sql: +// """ +// CREATE TRIGGER IF NOT EXISTS prevent_closed_span_modification +// BEFORE UPDATE ON \(SpanRecord.databaseTableName) +// WHEN OLD.end_time IS NOT NULL +// BEGIN +// SELECT RAISE(ABORT,'Attempted to modify an already closed span.'); +// END; +// """ ) +// } +// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/Migrations+Current.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/Migrations+Current.swift index 5c80da7d..354b136b 100644 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/Migrations+Current.swift +++ b/Sources/EmbraceStorageInternal/Migration/Migrations/Migrations+Current.swift @@ -1,20 +1,20 @@ +//// +//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +//// // -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// import Foundation // - -import Foundation - -extension Array where Element == Migration { - - public static var current: [Migration] { - return [ - // register migrations here - // order matters - AddSpanRecordMigration(), - AddSessionRecordMigration(), - AddMetadataRecordMigration(), - AddLogRecordMigration(), - AddProcessIdentifierToSpanRecordMigration() - ] - } -} +// extension Array where Element == Migration { +// +// public static var current: [Migration] { +// return [ +// // register migrations here +// // order matters +// AddSpanRecordMigration(), +// AddSessionRecordMigration(), +// AddMetadataRecordMigration(), +// AddLogRecordMigration(), +// AddProcessIdentifierToSpanRecordMigration() +// ] +// } +// } diff --git a/Sources/EmbraceStorageInternal/Queries/SpanRecord+SessionQuery.swift b/Sources/EmbraceStorageInternal/Queries/SpanRecord+SessionQuery.swift deleted file mode 100644 index fea7de01..00000000 --- a/Sources/EmbraceStorageInternal/Queries/SpanRecord+SessionQuery.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import GRDB - -extension SpanRecord { - /// Build QueryInterfaceRequest for SpanRecord that will query for: - /// ```swift - /// if session.coldStart - /// // spans that occur within same process that start before the session endTime - /// else - /// // spans that overlap with session start/end times - /// ``` - /// - /// Parameters: - /// - session: The session record to use as context when querying for spans - static func filter(for session: SessionRecord) -> QueryInterfaceRequest { - let sessionEndTime = session.endTime ?? session.lastHeartbeatTime - - if session.coldStart { - return SpanRecord.filter( - // same process and starts before session ends - SpanRecord.Schema.processIdentifier == session.processId && - SpanRecord.Schema.startTime <= sessionEndTime ) - - } else { - return SpanRecord.filter( - overlappingStart(startTime: session.startTime) || - entirelyWithin(startTime: session.startTime, endTime: sessionEndTime) || - overlappingEnd(endTime: sessionEndTime) || - entirelyOverlapped(startTime: session.startTime, endTime: sessionEndTime) - ) - } - } - - /// Where `Span.startTime` occurs before session start and `Span.endTime` occurs after session start or has not ended - private static func overlappingStart(startTime: Date) -> SQLExpression { - SpanRecord.Schema.startTime <= startTime && - SpanRecord.Schema.endTime >= startTime - } - - /// Where both `Span.startTime` and `Span.endTime` occur after session start and before session end - private static func entirelyWithin(startTime: Date, endTime: Date) -> SQLExpression { - SpanRecord.Schema.startTime >= startTime && - (SpanRecord.Schema.endTime <= endTime || SpanRecord.Schema.endTime == nil) - } - - /// Where `Span.startTime` occurs before session end and `Span.endTime` occurs after session end or has not ended - private static func overlappingEnd(endTime: Date) -> SQLExpression { - SpanRecord.Schema.startTime <= endTime && - (SpanRecord.Schema.endTime >= endTime || SpanRecord.Schema.endTime == nil) - } - - /// Where `Span.startTime` occurs before session start and `Span.endTime` occurs after session end or Span has not ended - private static func entirelyOverlapped(startTime: Date, endTime: Date) -> SQLExpression { - SpanRecord.Schema.startTime <= startTime && - (SpanRecord.Schema.endTime >= endTime || SpanRecord.Schema.endTime == nil) - } -} diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift index 3e9fe3e1..4dd59a79 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift @@ -28,120 +28,103 @@ extension EmbraceStorage { lifespanId: String = "" ) throws -> MetadataRecord? { - let metadata = MetadataRecord( + // update existing? + if let metadata = updateMetadata( key: key, - value: .string(value), + value: value, type: type, lifespan: lifespan, lifespanId: lifespanId - ) - - if try addMetadata(metadata) { + ) { return metadata } - return nil - } - - /// Adds a new `MetadataRecord`. - /// Fails and returns nil if the metadata limit was reached. - public func addMetadata(_ metadata: MetadataRecord) throws -> Bool { - try dbQueue.write { db in - - // required resources are always inserted - if metadata.type == .requiredResource { - try metadata.insert(db) - return true - } - - // check limit for the metadata type - // only records of the same type with the same lifespan id - // or permanent records of the same type - // this means a resource will not count towards the custom property limit, and viceversa - // this also means metadata from other sessions/processes will not count for the limit either - - let limit = limitForType(metadata.type) - let count = try MetadataRecord.filter( - MetadataRecord.Schema.type == metadata.type.rawValue && - (MetadataRecord.Schema.lifespan == MetadataRecordLifespan.permanent.rawValue || - MetadataRecord.Schema.lifespanId == metadata.lifespanId) - ).fetchCount(db) - - guard count < limit else { - // TODO: limit could be applied incorrectly if at max limit and updating an existing record - return false - } - - try metadata.insert(db) - return true + // create new + guard shouldAddMetadata(type: type, lifespanId: lifespanId) else { + return nil } + + let metadata = MetadataRecord.create( + context: coreData.context, + key: key, + value: value, + type: type, + lifespan: lifespan, + lifespanId: lifespanId + ) + + coreData.save() + + return metadata } - private func limitForType(_ type: MetadataRecordType) -> Int { - switch type { - case .resource: return options.resourcesLimit - case .customProperty: return options.customPropertiesLimit - case .personaTag: return options.personaTagsLimit - default: return 0 - } + + /// Returns the `MetadataRecord` for the given values. + public func fetchMetadata( + key: String, + type: MetadataRecordType, + lifespan: MetadataRecordLifespan, + lifespanId: String = "" + ) -> MetadataRecord? { + + let request = MetadataRecord.createFetchRequest() + request.fetchLimit = 1 + request.predicate = NSPredicate( + format: "key == %@ AND typeRaw == %@ AND lifespanRaw == %@ AND lifespanId == %@", + key, + type.rawValue, + lifespan.rawValue, + lifespanId + ) + + return coreData.fetch(withRequest: request).first } /// Updates the `MetadataRecord` for the given key, type and lifespan with a new given value. + /// - Returns: The updated record, if any public func updateMetadata( key: String, value: String, type: MetadataRecordType, lifespan: MetadataRecordLifespan, lifespanId: String - ) throws { + ) -> MetadataRecord? { - try dbQueue.write { db in - guard var record = try MetadataRecord - .filter( - MetadataRecord.Schema.key == key && - MetadataRecord.Schema.type == type.rawValue && - MetadataRecord.Schema.lifespan == lifespan.rawValue && - MetadataRecord.Schema.lifespanId == lifespanId - ) - .fetchOne(db) else { - return - } - - record.value = .string(value) - try record.update(db) + guard let metadata = fetchMetadata(key: key, type: type, lifespan: lifespan, lifespanId: lifespanId) else { + return nil } - } - /// Updates the given `MetadataRecord`. - public func updateMetadata(_ record: MetadataRecord) throws { - try dbQueue.write { db in - try record.update(db) - } + metadata.value = value + coreData.save() + + return metadata } - /// Removes all `MetadataRecords` that don't corresponde to the given session and process ids. + /// Removes all `MetadataRecords` that don't correspond to the given session and process ids. /// Permanent metadata is not removed. - public func cleanMetadata( - currentSessionId: String?, - currentProcessId: String - ) throws { - _ = try dbQueue.write { db in - if let currentSessionId = currentSessionId { - try MetadataRecord.filter( - (MetadataRecord.Schema.lifespan == MetadataRecordLifespan.session.rawValue && - MetadataRecord.Schema.lifespanId != currentSessionId) || - (MetadataRecord.Schema.lifespan == MetadataRecordLifespan.process.rawValue && - MetadataRecord.Schema.lifespanId != currentProcessId) - ) - .deleteAll(db) - } else { - try MetadataRecord.filter( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.process.rawValue && - MetadataRecord.Schema.lifespanId != currentProcessId - ) - .deleteAll(db) - } + public func cleanMetadata(currentSessionId: String?, currentProcessId: String) { + let request = MetadataRecord.createFetchRequest() + + let processIdPredicate = NSPredicate( + format: "lifespanRaw == %@ AND lifespanId != %@", + MetadataRecordLifespan.process.rawValue, + currentProcessId + ) + + if let currentSessionId = currentSessionId { + let sessionIdPredicate = NSPredicate( + format: "lifespanRaw == %@ AND lifespanId != %@", + MetadataRecordLifespan.session.rawValue, + currentSessionId + ) + + request.predicate = NSCompoundPredicate(type: .or, subpredicates: [sessionIdPredicate, processIdPredicate]) + } else { + request.predicate = processIdPredicate } + + let records = coreData.fetch(withRequest: request) + coreData.deleteRecords(records) } /// Removes the `MetadataRecord` for the given values. @@ -151,207 +134,267 @@ extension EmbraceStorage { lifespan: MetadataRecordLifespan, lifespanId: String ) throws { - _ = try dbQueue.write { db in - try MetadataRecord - .filter( - MetadataRecord.Schema.key == key && - MetadataRecord.Schema.type == type.rawValue && - MetadataRecord.Schema.lifespan == lifespan.rawValue && - MetadataRecord.Schema.lifespanId == lifespanId - ) - .deleteAll(db) + + guard let metadata = fetchMetadata(key: key, type: type, lifespan: lifespan, lifespanId: lifespanId) else { + return } + + coreData.deleteRecord(metadata) } - /// Removes all `MetadataRecords` for the given lifespans. + /// Removes all `MetadataRecords` for the given type and lifespans. /// - Note: This method is inteded to be indirectly used by implementers of the SDK /// For this reason records of the `.requiredResource` type are not removed. - public func removeAllMetadata(type: MetadataRecordType, lifespans: [MetadataRecordLifespan]) throws { + public func removeAllMetadata(type: MetadataRecordType, lifespans: [MetadataRecordLifespan]) { guard type != .requiredResource && lifespans.count > 0 else { return } - try dbQueue.write { db in - let request = MetadataRecord.filter(MetadataRecord.Schema.type == type.rawValue) + let request = MetadataRecord.createFetchRequest() + let typePredicate = NSPredicate(format: "typeRaw == %@", type.rawValue) - var expressions: [SQLExpression] = [] - for lifespan in lifespans { - expressions.append(MetadataRecord.Schema.lifespan == lifespan.rawValue) - } - - try request - .filter(expressions.joined(operator: .or)) - .deleteAll(db) + var lifespanPredicates: [NSPredicate] = [] + for lifespan in lifespans { + let predicate = NSPredicate(format: "lifespanRaw == %@", lifespan.rawValue) + lifespanPredicates.append(predicate) } + let lifespansPredicate = NSCompoundPredicate(type: .or, subpredicates: lifespanPredicates) + + request.predicate = NSCompoundPredicate(type: .and, subpredicates: [typePredicate, lifespansPredicate]) + + let records = coreData.fetch(withRequest: request) + coreData.deleteRecords(records) } /// Removes all `MetadataRecords` for the given keys and timespan. - /// Note that this method is inteded to be indirectly used by implementers of the SDK - /// For this reason records of the `.requiredResource` type are not removed. - public func removeAllMetadata(keys: [String], lifespan: MetadataRecordLifespan) throws { + /// - Note: This method is inteded to be indirectly used by implementers of the SDK + /// For this reason records of the `.requiredResource` type are not removed. + public func removeAllMetadata(keys: [String], lifespan: MetadataRecordLifespan) { guard keys.count > 0 else { return } - try dbQueue.write { db in - let request = MetadataRecord.filter( - MetadataRecord.Schema.type != MetadataRecordType.requiredResource.rawValue - ) - - var expressions: [SQLExpression] = [] - for key in keys { - expressions.append(MetadataRecord.Schema.key == key) - } + let request = MetadataRecord.createFetchRequest() + let typePredicate = NSPredicate(format: "typeRaw != %@", MetadataRecordType.requiredResource.rawValue) - try request - .filter(expressions.joined(operator: .or)) - .deleteAll(db) + var keyPredicates: [NSPredicate] = [] + for key in keys { + let predicate = NSPredicate(format: "key == %@", key) + keyPredicates.append(predicate) } - } + let keyPredicate = NSCompoundPredicate(type: .or, subpredicates: keyPredicates) - /// Returns the `MetadataRecord` for the given values. - public func fetchMetadata( - key: String, - type: MetadataRecordType, - lifespan: MetadataRecordLifespan, - lifespanId: String = "" - ) throws -> MetadataRecord? { + request.predicate = NSCompoundPredicate(type: .and, subpredicates: [typePredicate, keyPredicate]) - try dbQueue.read { db in - return try MetadataRecord - .filter( - MetadataRecord.Schema.key == key && - MetadataRecord.Schema.type == type.rawValue && - MetadataRecord.Schema.lifespan == lifespan.rawValue && - MetadataRecord.Schema.lifespanId == lifespanId - ) - .fetchOne(db) - } + let records = coreData.fetch(withRequest: request) + coreData.deleteRecords(records) } /// Returns the permanent required resource for the given key. public func fetchRequiredPermanentResource(key: String) throws -> MetadataRecord? { - return try fetchMetadata(key: key, type: .requiredResource, lifespan: .permanent) + return fetchMetadata(key: key, type: .requiredResource, lifespan: .permanent) } /// Returns all records with types `.requiredResource` or `.resource` public func fetchAllResources() throws -> [MetadataRecord] { - try dbQueue.read { db in - return try resourcesFilter().fetchAll(db) - } + let request = MetadataRecord.createFetchRequest() + request.predicate = NSPredicate( + format: "typeRaw == %@ OR typeRaw == %@", + MetadataRecordType.resource.rawValue, + MetadataRecordType.requiredResource.rawValue + ) + + return coreData.fetch(withRequest: request) } /// Returns all records with types `.requiredResource` or `.resource` that are tied to a given session id public func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { - try dbQueue.read { db in - guard let session = try SessionRecord.fetchOne(db, key: sessionId.toString) else { - return [] - } - - return try resourcesFilter() - .filter( - ( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.session.rawValue && - MetadataRecord.Schema.lifespanId == session.id.toString - ) || ( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.process.rawValue && - MetadataRecord.Schema.lifespanId == session.processId.hex - ) || - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.permanent.rawValue - ) - .fetchAll(db) + + guard let session = fetchSession(id: sessionId) else { + return [] } + + let request = MetadataRecord.createFetchRequest() + request.predicate = NSCompoundPredicate( + type: .and, + subpredicates:[ + resourcePredicate(), + lifespanPredicate(session: session) + ] + ) + + return coreData.fetch(withRequest: request) } /// Returns all records with types `.requiredResource` or `.resource` that are tied to a given process id public func fetchResourcesForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] { - try dbQueue.read { db in - - return try resourcesFilter() - .filter( - ( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.process.rawValue && - MetadataRecord.Schema.lifespanId == processId.hex - ) || - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.permanent.rawValue - ) - .fetchAll(db) - } + + let request = MetadataRecord.createFetchRequest() + request.predicate = NSCompoundPredicate( + type: .and, + subpredicates:[ + resourcePredicate(), + lifespanPredicate(processId: processId) + ] + ) + + return coreData.fetch(withRequest: request) } /// Returns all records of the `.customProperty` type that are tied to a given session id public func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { - try dbQueue.read { db in - guard let session = try SessionRecord.fetchOne(db, key: sessionId.toString) else { - return [] - } - - return try customPropertiesFilter() - .filter( - ( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.session.rawValue && - MetadataRecord.Schema.lifespanId == session.id.toString - ) || ( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.process.rawValue && - MetadataRecord.Schema.lifespanId == session.processId.hex - ) || - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.permanent.rawValue - ) - .fetchAll(db) + guard let session = fetchSession(id: sessionId) else { + return [] } + + let request = MetadataRecord.createFetchRequest() + request.predicate = NSCompoundPredicate( + type: .and, + subpredicates:[ + customPropertyPredicate(), + lifespanPredicate(session: session) + ] + ) + + return coreData.fetch(withRequest: request) } /// Returns all records of the `.personaTag` type that are tied to a given session id public func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { - try dbQueue.read { db in - guard let session = try SessionRecord.fetchOne(db, key: sessionId.toString) else { - return [] - } - - return try personaTagsFilter() - .filter( - ( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.session.rawValue && - MetadataRecord.Schema.lifespanId == session.id.toString - ) || ( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.process.rawValue && - MetadataRecord.Schema.lifespanId == session.processId.hex - ) || - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.permanent.rawValue - ) - .fetchAll(db) + guard let session = fetchSession(id: sessionId) else { + return [] } + + let request = MetadataRecord.createFetchRequest() + request.predicate = NSCompoundPredicate( + type: .and, + subpredicates:[ + personaTagPredicate(), + lifespanPredicate(session: session) + ] + ) + + return coreData.fetch(withRequest: request) } /// Returns all records of the `.personaTag` type that are tied to a given process id public func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] { - try dbQueue.read { db in - - return try personaTagsFilter() - .filter( - ( - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.process.rawValue && - MetadataRecord.Schema.lifespanId == processId.hex - ) || - MetadataRecord.Schema.lifespan == MetadataRecordLifespan.permanent.rawValue - ) - .fetchAll(db) - } + + let request = MetadataRecord.createFetchRequest() + request.predicate = NSCompoundPredicate( + type: .and, + subpredicates:[ + personaTagPredicate(), + lifespanPredicate(processId: processId) + ] + ) + + return coreData.fetch(withRequest: request) } } extension EmbraceStorage { - private func resourcesFilter() -> QueryInterfaceRequest { - MetadataRecord.filter( - MetadataRecord.Schema.type == MetadataRecordType.requiredResource.rawValue || - MetadataRecord.Schema.type == MetadataRecordType.resource.rawValue) + + /// Adds a new `MetadataRecord`. + /// Fails and returns nil if the metadata limit was reached. + public func shouldAddMetadata(type: MetadataRecordType, lifespanId: String) -> Bool { + + // required resources are always inserted + if type == .requiredResource { + return true + } + + // check limit for the metadata type + // only records of the same type with the same lifespan id + // or permanent records of the same type + // this means a resource will not count towards the custom property limit, and viceversa + // this also means metadata from other sessions/processes will not count for the limit either + let request = MetadataRecord.createFetchRequest() + request.predicate = NSPredicate( + format: "typeRaw == %@ AND (lifespanRaw == %@ OR lifespanId == %@)", + type.rawValue, + MetadataRecordLifespan.permanent.rawValue, + lifespanId + ) + + let limit = limitForType(type) + return coreData.count(withRequest: request) < limit + } + + private func limitForType(_ type: MetadataRecordType) -> Int { + switch type { + case .resource: return options.resourcesLimit + case .customProperty: return options.customPropertiesLimit + case .personaTag: return options.personaTagsLimit + default: return 0 + } + } + + private func resourcePredicate() -> NSPredicate { + return NSPredicate( + format: "typeRaw == %@ OR typeRaw == %@", + MetadataRecordType.resource.rawValue, + MetadataRecordType.requiredResource.rawValue + ) } - private func customPropertiesFilter() -> QueryInterfaceRequest { - MetadataRecord.filter(MetadataRecord.Schema.type == MetadataRecordType.customProperty.rawValue) + private func customPropertyPredicate() -> NSPredicate { + return NSPredicate(format: "typeRaw == %@", MetadataRecordType.customProperty.rawValue) } - private func personaTagsFilter() -> QueryInterfaceRequest { - MetadataRecord.filter(MetadataRecord.Schema.type == MetadataRecordType.personaTag.rawValue) + private func personaTagPredicate() -> NSPredicate { + return NSPredicate(format: "typeRaw == %@", MetadataRecordType.personaTag.rawValue) } + + private func lifespanPredicate(session: SessionRecord) -> NSPredicate { + // match the session id + let sessionIdPredicate = NSPredicate( + format: "lifespanRaw == %@ AND lifespanId == %@", + MetadataRecordLifespan.session.rawValue, + session.idRaw + ) + // or match the process id + let processIdPredicate = NSPredicate( + format: "lifespanRaw == %@ AND lifespanId == %@", + MetadataRecordLifespan.process.rawValue, + session.processIdRaw + ) + // or are permanent + let permanentPredicate = NSPredicate( + format: "lifespanRaw == %@", + MetadataRecordLifespan.permanent.rawValue + ) + + return NSCompoundPredicate( + type: .or, + subpredicates: [ + sessionIdPredicate, + processIdPredicate, + permanentPredicate + ] + ) + } + + private func lifespanPredicate(processId: ProcessIdentifier) -> NSPredicate { + // match the process id + let processIdPredicate = NSPredicate( + format: "lifespanRaw == %@ AND lifespanId == %@", + MetadataRecordLifespan.process.rawValue, + processId.hex + ) + // or are permanent + let permanentPredicate = NSPredicate( + format: "lifespanRaw == %@", + MetadataRecordLifespan.permanent.rawValue + ) + + return NSCompoundPredicate( + type: .or, + subpredicates: [ + processIdPredicate, + permanentPredicate + ] + ) + } + } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift index 2a3913f4..7f5f1c8f 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift @@ -30,11 +30,32 @@ extension EmbraceStorage { endTime: Date? = nil, lastHeartbeatTime: Date? = nil, crashReportId: String? = nil - ) throws -> SessionRecord { - let session = SessionRecord( + ) -> SessionRecord { + + // update existing? + if let session = fetchSession(id: id) { + session.state = state.rawValue + session.processIdRaw = processId.hex + session.traceId = traceId + session.spanId = spanId + session.startTime = startTime + session.endTime = endTime + session.crashReportId = crashReportId + + if let lastHeartbeatTime = lastHeartbeatTime { + session.lastHeartbeatTime = lastHeartbeatTime + } + + coreData.save() + return session + } + + // create new + let session = SessionRecord.create( + context: coreData.context, id: id, - state: state, processId: processId, + state: state, traceId: traceId, spanId: spanId, startTime: startTime, @@ -42,60 +63,44 @@ extension EmbraceStorage { lastHeartbeatTime: lastHeartbeatTime ) - try upsertSession(session) + coreData.save() return session } - /// Adds or updates a `SessionRecord` to the storage synchronously. - /// - Parameter record: `SessionRecord` to insert - public func upsertSession(_ session: SessionRecord) throws { - try dbQueue.write { db in - try session.insert(db) - } - } - /// Fetches the stored `SessionRecord` synchronously with the given identifier, if any. /// - Parameters: /// - id: Identifier of the session /// - Returns: The stored `SessionRecord`, if any + public func fetchSession(id: SessionIdentifier) -> SessionRecord? { + let request = SessionRecord.createFetchRequest() + request.fetchLimit = 1 + request.predicate = NSPredicate(format: "idRaw == %@", id.toString) - public func fetchSession(id: SessionIdentifier) throws -> SessionRecord? { - try dbQueue.read { db in - return try SessionRecord.fetchOne(db, key: id) - } + return coreData.fetch(withRequest: request).first } /// Synchronously fetches the newest session in the storage, ignoring the current session if it exists. /// - Returns: The newest stored `SessionRecord`, if any - public func fetchLatestSession( - ignoringCurrentSessionId sessionId: SessionIdentifier? = nil - ) throws -> SessionRecord? { - var session: SessionRecord? - try dbQueue.read { db in + public func fetchLatestSession(ignoringCurrentSessionId sessionId: SessionIdentifier? = nil) -> SessionRecord? { + let request = SessionRecord.createFetchRequest() + request.fetchLimit = 1 + request.sortDescriptors = [NSSortDescriptor(key: "startTime", ascending: false)] - var filter = SessionRecord.order(SessionRecord.Schema.startTime.desc) - - if let sessionId = sessionId { - filter = filter.filter(SessionRecord.Schema.id != sessionId) - } - - session = try filter.fetchOne(db) + if let sessionId = sessionId { + request.predicate = NSPredicate(format: "idRaw != %@", sessionId.toString) } - return session + return coreData.fetch(withRequest: request).first } /// Synchronously fetches the oldest session in the storage, if any. /// - Returns: The oldest stored `SessionRecord`, if any - public func fetchOldestSession() throws -> SessionRecord? { - var session: SessionRecord? - try dbQueue.read { db in - session = try SessionRecord - .order(SessionRecord.Schema.startTime.asc) - .fetchOne(db) - } + public func fetchOldestSession() -> SessionRecord? { + let request = SessionRecord.createFetchRequest() + request.fetchLimit = 1 + request.sortDescriptors = [NSSortDescriptor(key: "startTime", ascending: true)] - return session + return coreData.fetch(withRequest: request).first } } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index 5f971b88..cb67c423 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -5,13 +5,14 @@ import Foundation import EmbraceCommonInternal import EmbraceSemantics +import CoreData import GRDB extension EmbraceStorage { static let defaultSpanLimitByType = 1500 - /// Adds a span to the storage synchronously. + /// Adds or updates a span to the storage synchronously. /// - Parameters: /// - id: Identifier of the span /// - name: name of the span @@ -22,7 +23,7 @@ extension EmbraceStorage { /// - endTime: Date of when the span ended (optional) /// - Returns: The newly stored `SpanRecord` @discardableResult - public func addSpan( + public func upsertSpan( id: String, name: String, traceId: String, @@ -31,9 +32,27 @@ extension EmbraceStorage { startTime: Date, endTime: Date? = nil, processIdentifier: ProcessIdentifier = .current - ) throws -> SpanRecord { + ) -> SpanRecord { + + // update existing? + if let span = fetchSpan(id: id, traceId: traceId) { + span.name = name + span.typeRaw = type.rawValue + span.data = data + span.startTime = startTime + span.endTime = endTime + span.processIdRaw = processIdentifier.hex + + coreData.save() + return span + } + + // make space if needed + removeOldSpanIfNeeded(forType: type) - let span = SpanRecord( + // add new + let span = SpanRecord.create( + context: coreData.context, id: id, name: name, traceId: traceId, @@ -43,24 +62,10 @@ extension EmbraceStorage { endTime: endTime, processIdentifier: processIdentifier ) - try upsertSpan(span) - return span - } + coreData.save() - /// Adds or updates a `SpanRecord` to the storage synchronously. - /// - Parameter record: `SpanRecord` to upsert - public func upsertSpan(_ span: SpanRecord) throws { - do { - try dbQueue.write { [weak self] db in - try self?.upsertSpan(db: db, span: span) - } - } catch let exception as DatabaseError { - throw EmbraceStorageError.cannotUpsertSpan( - spanName: span.name, - message: exception.message ?? "[empty message]" - ) - } + return span } /// Fetches the stored `SpanRecord` synchronously with the given identifiers, if any. @@ -68,48 +73,44 @@ extension EmbraceStorage { /// - id: Identifier of the span /// - traceId: Identifier of the trace containing this span /// - Returns: The stored `SpanRecord`, if any - public func fetchSpan(id: String, traceId: String) throws -> SpanRecord? { - var span: SpanRecord? - try dbQueue.read { db in - span = try SpanRecord.fetchOne( - db, - key: [ - SpanRecord.Schema.traceId.name: traceId, - SpanRecord.Schema.id.name: id - ] - ) - } + public func fetchSpan(id: String, traceId: String) -> SpanRecord? { + let request = SpanRecord.createFetchRequest() + request.fetchLimit = 1 + request.predicate = NSPredicate(format: "id == %@ AND traceId == %i", id, traceId) - return span + return coreData.fetch(withRequest: request).first } /// Synchronously removes all the closed spans older than the given date. /// If no date is provided, all closed spans will be removed. /// - Parameter date: Date used to determine which spans to remove - public func cleanUpSpans(date: Date? = nil) throws { - _ = try dbQueue.write { db in - var filter = SpanRecord.filter(SpanRecord.Schema.endTime != nil) + public func cleanUpSpans(date: Date? = nil) { + let request = SpanRecord.createFetchRequest() - if let date = date { - filter = filter.filter(SpanRecord.Schema.endTime < date) - } - - try filter.deleteAll(db) + if let date = date { + request.predicate = NSPredicate(format: "date != nil AND date < %@", date as NSDate) + } else { + request.predicate = NSPredicate(format: "date != nil") } + + let spans = coreData.fetch(withRequest: request) + coreData.deleteRecords(spans) } /// Synchronously closes all open spans with the given `endTime`. /// - Parameters: /// - endTime: Identifier of the trace containing this span - public func closeOpenSpans(endTime: Date) throws { - _ = try dbQueue.write { db in - try SpanRecord - .filter( - SpanRecord.Schema.endTime == nil && - SpanRecord.Schema.processIdentifier != ProcessIdentifier.current - ) - .updateAll(db, SpanRecord.Schema.endTime.set(to: endTime)) + public func closeOpenSpans(endTime: Date) { + let request = SpanRecord.createFetchRequest() + request.predicate = NSPredicate(format: "date = nil") + + let spans = coreData.fetch(withRequest: request) + + for span in spans { + span.endTime = endTime } + + coreData.save() } /// Fetch spans for the given session record @@ -122,119 +123,70 @@ extension EmbraceStorage { for sessionRecord: SessionRecord, ignoreSessionSpans: Bool = true, limit: Int = 1000 - ) throws -> [SpanRecord] { - return try dbQueue.read { db in - var query = SpanRecord.filter(for: sessionRecord) + ) -> [SpanRecord] { - if ignoreSessionSpans { - query = query.filter(SpanRecord.Schema.type != SpanType.session) - } + let request = SpanRecord.createFetchRequest() + request.fetchLimit = limit - return try query - .limit(limit) - .fetchAll(db) - } - } -} + let endTime = (sessionRecord.endTime ?? sessionRecord.lastHeartbeatTime) as NSDate -// MARK: - Database operations -fileprivate extension EmbraceStorage { - func upsertSpan(db: Database, span: SpanRecord) throws { - // update if its already stored - if try span.exists(db) { - try span.update(db) - return - } - - // check limit and delete if necessary - // default to 1500 if limit is not set - let limit = options.spanLimits[span.type, default: Self.defaultSpanLimitByType] - - let count = try spanCount(db: db, type: span.type) - if count >= limit { - let spansToDelete = try fetchSpans( - db: db, - type: span.type, - limit: count - limit + 1 + // special case for cold start sessions + // we grab spans that might have started before the session but within the same process + if sessionRecord.coldStart { + request.predicate = NSPredicate( + format: "processIdRaw == %@ AND startTime <= %@", + sessionRecord.processIdRaw, + endTime ) - - for spanToDelete in spansToDelete { - try spanToDelete.delete(db) - } } - try span.insert(db) - } - - func requestSpans(of type: SpanType) -> QueryInterfaceRequest { - return SpanRecord.filter(SpanRecord.Schema.type == type.rawValue) - } - - func spanCount(db: Database, type: SpanType) throws -> Int { - return try requestSpans(of: type) - .fetchCount(db) - } + // otherwise we check if the span is within the boundaries of the session + else { + let startTime = sessionRecord.startTime as NSDate + + // span starts within session and + // - ends before session ends or + // - hasn't ended yet + let predicate1 = NSPredicate( + format: "(startTime >= %@ AND (endTime = nil OR endTime <= %@)", + startTime, + endTime + ) - func fetchSpans(db: Database, type: SpanType, limit: Int?) throws -> [SpanRecord] { - var request = requestSpans(of: type) - .order(SpanRecord.Schema.startTime) + // span starts before session and + // - ends within session or + // - hasn't ended yet + let predicate2 = NSPredicate( + format: "(startTime < %@ AND (endTime = nil OR (endTime >= %@ AND endTime <= %@))", + startTime, + startTime, + endTime + ) - if let limit = limit { - request = request.limit(limit) + request.predicate = NSCompoundPredicate(type: .or, subpredicates: [predicate1, predicate2]) } - return try request.fetchAll(db) + return coreData.fetch(withRequest: request) } +} - func spanInTimeFrameByTypeRequest( - startTime: Date, - endTime: Date, - includeOlder: Bool, - ignoreSessionSpans: Bool - ) -> QueryInterfaceRequest { - - // end_time is nil - // or end_time is between parameters (start_time, end_time) - var filter = SpanRecord.filter( - SpanRecord.Schema.endTime == nil || - (SpanRecord.Schema.endTime <= endTime && SpanRecord.Schema.endTime >= startTime) - ) - - // if we don't include old spans - // select where start_time is greater than parameter (start_time) - if includeOlder == false { - filter = filter.filter(SpanRecord.Schema.startTime >= startTime) - } - - // if ignoring session span - // select where type is not session span - if ignoreSessionSpans == true { - filter = filter.filter(SpanRecord.Schema.type != SpanType.session.rawValue) - } - - return filter - } +// MARK: - Database operations +fileprivate extension EmbraceStorage { + func removeOldSpanIfNeeded(forType type: SpanType) { + // check limit and delete if necessary + // default to 1500 if limit is not set + let limit = options.spanLimits[type, default: Self.defaultSpanLimitByType] - func fetchSpans( - db: Database, - startTime: Date, - endTime: Date, - includeOlder: Bool, - ignoreSessionSpans: Bool, - limit: Int? - ) throws -> [SpanRecord] { + let request = SpanRecord.createFetchRequest() + request.predicate = NSPredicate(format: "typeRaw == %@", type.rawValue) + let count = coreData.count(withRequest: request) - var request = spanInTimeFrameByTypeRequest( - startTime: startTime, - endTime: endTime, - includeOlder: includeOlder, - ignoreSessionSpans: ignoreSessionSpans - ).order(SpanRecord.Schema.startTime) + if count >= limit { + request.fetchLimit = count - limit + 1 + request.sortDescriptors = [ NSSortDescriptor(key: "startTime", ascending: true) ] - if let limit = limit { - request = request.limit(limit) + let spansToDelete = coreData.fetch(withRequest: request) + coreData.deleteRecords(spansToDelete) } - - return try request.fetchAll(db) } } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift new file mode 100644 index 00000000..bc98d79b --- /dev/null +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift @@ -0,0 +1,11 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import CoreData + +public protocol EmbraceStorageRecord: NSManagedObject { + static var entityName: String { get } + static var entityDescription: NSEntityDescription { get } +} diff --git a/Sources/EmbraceStorageInternal/Records/MetadataRecord+ValueTypes.swift b/Sources/EmbraceStorageInternal/Records/MetadataRecord+ValueTypes.swift deleted file mode 100644 index eaa86fb4..00000000 --- a/Sources/EmbraceStorageInternal/Records/MetadataRecord+ValueTypes.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation - -extension MetadataRecord { - - public var boolValue: Bool? { - switch value { - case .bool(let bool): return bool - case .int(let integer): return integer > 0 - case .double(let double): return double > 0 - case .string(let string): return Bool(string) - default: return nil - } - } - - public var integerValue: Int? { - switch value { - case .bool(let bool): return bool ? 1 : 0 - case .int(let integer): return integer - case .double(let double): return Int(double) - case .string(let string): return Int(string) - default: return nil - } - } - - public var doubleValue: Double? { - switch value { - case .bool(let bool): return bool ? 1 : 0 - case .int(let integer): return Double(integer) - case .double(let double): return double - case .string(let string): return Double(string) - default: return nil - } - } - - public var stringValue: String? { - switch value { - case .bool(let bool): return String(bool) - case .int(let integer): return String(integer) - case .double(let double): return String(double) - case .string(let string): return string - default: return nil - } - } - - public var uuidValue: UUID? { - switch value { - case .string(let string): return UUID(withoutHyphen: string) - default: return nil - } - } -} diff --git a/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift b/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift index f830f923..dfc44fca 100644 --- a/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift @@ -4,7 +4,7 @@ import Foundation import EmbraceCommonInternal -import GRDB +import CoreData import OpenTelemetryApi public enum MetadataRecordType: String, Codable { @@ -32,54 +32,89 @@ public enum MetadataRecordLifespan: String, Codable { case permanent } -public struct MetadataRecord: Codable { - public let key: String - public var value: AttributeValue - public let type: MetadataRecordType - public let lifespan: MetadataRecordLifespan - public let lifespanId: String - public let collectedAt: Date +public class MetadataRecord: NSManagedObject { + @NSManaged public var key: String + @NSManaged public var value: String + @NSManaged public var typeRaw: String // MetadataRecordType + @NSManaged public var lifespanRaw: String // MetadataRecordLifespan + @NSManaged public var lifespanId: String + @NSManaged public var collectedAt: Date - /// Main initializer for the MetadataRecord - public init( + public var type: MetadataRecordType? { + return MetadataRecordType(rawValue: typeRaw) + } + + public var lifespan: MetadataRecordLifespan? { + return MetadataRecordLifespan(rawValue: lifespanRaw) + } + + public static func create( + context: NSManagedObjectContext, key: String, - value: AttributeValue, + value: String, type: MetadataRecordType, lifespan: MetadataRecordLifespan, lifespanId: String, collectedAt: Date = Date() - ) { - self.key = key - self.value = value - self.type = type - self.lifespan = lifespan - self.lifespanId = lifespanId - self.collectedAt = collectedAt + ) -> MetadataRecord { + let record = MetadataRecord(context: context) + record.key = key + record.value = value + record.typeRaw = type.rawValue + record.lifespanRaw = lifespan.rawValue + record.lifespanId = lifespanId + record.collectedAt = collectedAt + + return record } -} -extension MetadataRecord { - struct Schema { - static var key: Column { Column("key") } - static var value: Column { Column("value") } - static var type: Column { Column("type") } - static var lifespan: Column { Column("lifespan") } - static var lifespanId: Column { Column("lifespan_id") } - static var collectedAt: Column { Column("collected_at") } + static func createFetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: entityName) } } -extension MetadataRecord: FetchableRecord, PersistableRecord, MutablePersistableRecord { - public static let databaseTableName: String = "metadata" +extension MetadataRecord: EmbraceStorageRecord { + public static var entityName = "MetadataRecord" - public static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase - public static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase - public static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .replace, update: .replace) -} + static public var entityDescription: NSEntityDescription { + let entity = NSEntityDescription() + entity.name = entityName + entity.managedObjectClassName = NSStringFromClass(MetadataRecord.self) + + let keyAttribute = NSAttributeDescription() + keyAttribute.name = "key" + keyAttribute.attributeType = .stringAttributeType + + let valueAttribute = NSAttributeDescription() + valueAttribute.name = "value" + valueAttribute.attributeType = .stringAttributeType + + let typeAttribute = NSAttributeDescription() + typeAttribute.name = "typeRaw" + typeAttribute.attributeType = .stringAttributeType + + let lifespanAttribute = NSAttributeDescription() + lifespanAttribute.name = "lifespanRaw" + lifespanAttribute.attributeType = .stringAttributeType + + let lifespanIdAttribute = NSAttributeDescription() + lifespanIdAttribute.name = "lifespanId" + lifespanIdAttribute.attributeType = .stringAttributeType + + let collectedAtAttribute = NSAttributeDescription() + collectedAtAttribute.name = "collectedAt" + collectedAtAttribute.attributeType = .dateAttributeType + + entity.properties = [ + keyAttribute, + valueAttribute, + typeAttribute, + lifespanAttribute, + lifespanIdAttribute, + collectedAtAttribute + ] -extension MetadataRecord: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.key == rhs.key + return entity } } diff --git a/Sources/EmbraceStorageInternal/Records/SessionRecord.swift b/Sources/EmbraceStorageInternal/Records/SessionRecord.swift index 7ca49315..b72931c8 100644 --- a/Sources/EmbraceStorageInternal/Records/SessionRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/SessionRecord.swift @@ -4,33 +4,42 @@ import Foundation import EmbraceCommonInternal -import GRDB +import CoreData /// Represents a session in the storage -public struct SessionRecord: Codable { - public var id: SessionIdentifier - public var processId: ProcessIdentifier - public var state: String - public var traceId: String - public var spanId: String - public var startTime: Date - public var endTime: Date? - public var lastHeartbeatTime: Date - public var crashReportId: String? +public class SessionRecord: NSManagedObject { + @NSManaged public var idRaw: String // SessionIdentifier + @NSManaged public var processIdRaw: String // ProcessIdentifier + @NSManaged public var state: String + @NSManaged public var traceId: String + @NSManaged public var spanId: String + @NSManaged public var startTime: Date + @NSManaged public var endTime: Date? + @NSManaged public var lastHeartbeatTime: Date + @NSManaged public var crashReportId: String? /// Used to mark if the session is the first to occur during this process - public var coldStart: Bool + @NSManaged public var coldStart: Bool /// Used to mark the session ended in an expected manner - public var cleanExit: Bool + @NSManaged public var cleanExit: Bool /// Used to mark the session that is active when the application was explicitly terminated by the user and/or system - public var appTerminated: Bool + @NSManaged public var appTerminated: Bool - public init( + public var id: SessionIdentifier? { + return SessionIdentifier(string: idRaw) + } + + public var processId: ProcessIdentifier? { + return ProcessIdentifier(hex: processIdRaw) + } + + public static func create( + context: NSManagedObjectContext, id: SessionIdentifier, - state: SessionState, processId: ProcessIdentifier, + state: SessionState, traceId: String, spanId: String, startTime: Date, @@ -39,50 +48,101 @@ public struct SessionRecord: Codable { crashReportId: String? = nil, coldStart: Bool = false, cleanExit: Bool = false, - appTerminated: Bool = false) { - - self.id = id - self.state = state.rawValue - self.processId = processId - self.traceId = traceId - self.spanId = spanId - self.startTime = startTime - self.endTime = endTime - self.lastHeartbeatTime = lastHeartbeatTime ?? startTime - self.crashReportId = crashReportId - self.coldStart = coldStart - self.cleanExit = cleanExit - self.appTerminated = appTerminated + appTerminated: Bool = false + ) -> SessionRecord { + let record = SessionRecord(context: context) + record.idRaw = id.toString + record.processIdRaw = processId.hex + record.state = state.rawValue + record.traceId = traceId + record.spanId = spanId + record.startTime = startTime + record.endTime = endTime + record.lastHeartbeatTime = lastHeartbeatTime ?? startTime + record.crashReportId = crashReportId + record.coldStart = coldStart + record.cleanExit = cleanExit + record.appTerminated = appTerminated + + return record } -} -extension SessionRecord { - struct Schema { - static var id: Column { Column("id") } - static var state: Column { Column("state") } - static var processId: Column { Column("process_id") } - static var traceId: Column { Column("trace_id") } - static var spanId: Column { Column("span_id") } - static var startTime: Column { Column("start_time") } - static var endTime: Column { Column("end_time") } - static var lastHeartbeatTime: Column { Column("last_heartbeat_time") } - static var crashReportId: Column { Column("crash_report_id") } - static var coldStart: Column { Column("cold_start") } - static var cleanExit: Column { Column("clean_exit") } - static var appTerminated: Column { Column("app_terminated") } + static func createFetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: entityName) } } -extension SessionRecord: FetchableRecord, PersistableRecord, MutablePersistableRecord { - public static let databaseTableName: String = "sessions" +extension SessionRecord: EmbraceStorageRecord { + public static var entityName = "SessionRecord" - public static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase - public static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase - public static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .replace, update: .replace) -} + static public var entityDescription: NSEntityDescription { + let entity = NSEntityDescription() + entity.name = entityName + entity.managedObjectClassName = NSStringFromClass(SessionRecord.self) + + let idAttribute = NSAttributeDescription() + idAttribute.name = "idRaw" + idAttribute.attributeType = .stringAttributeType + + let processIdAttribute = NSAttributeDescription() + processIdAttribute.name = "processIdRaw" + processIdAttribute.attributeType = .stringAttributeType + + let stateAttribute = NSAttributeDescription() + stateAttribute.name = "state" + stateAttribute.attributeType = .stringAttributeType + + let traceIdAttribute = NSAttributeDescription() + traceIdAttribute.name = "traceId" + traceIdAttribute.attributeType = .stringAttributeType + + let spanIdAttribute = NSAttributeDescription() + spanIdAttribute.name = "spanId" + spanIdAttribute.attributeType = .stringAttributeType + + let startTimeAttribute = NSAttributeDescription() + startTimeAttribute.name = "startTime" + startTimeAttribute.attributeType = .dateAttributeType + + let endTimeAttribute = NSAttributeDescription() + endTimeAttribute.name = "endTime" + endTimeAttribute.attributeType = .dateAttributeType + + let lastHeartbeatTimeAttribute = NSAttributeDescription() + lastHeartbeatTimeAttribute.name = "lastHeartbeatTime" + lastHeartbeatTimeAttribute.attributeType = .dateAttributeType + + let crashReportIdAttribute = NSAttributeDescription() + crashReportIdAttribute.name = "crashReportId" + crashReportIdAttribute.attributeType = .stringAttributeType + + let coldStartAttribute = NSAttributeDescription() + coldStartAttribute.name = "coldStart" + coldStartAttribute.attributeType = .booleanAttributeType + + let cleanExitAttribute = NSAttributeDescription() + cleanExitAttribute.name = "cleanExit" + cleanExitAttribute.attributeType = .booleanAttributeType + + let appTerminatedAttribute = NSAttributeDescription() + appTerminatedAttribute.name = "appTerminated" + appTerminatedAttribute.attributeType = .booleanAttributeType + + entity.properties = [ + idAttribute, + processIdAttribute, + stateAttribute, + traceIdAttribute, + spanIdAttribute, + startTimeAttribute, + endTimeAttribute, + lastHeartbeatTimeAttribute, + crashReportIdAttribute, + coldStartAttribute, + cleanExitAttribute, + appTerminatedAttribute + ] -extension SessionRecord: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.id == rhs.id + return entity } } diff --git a/Sources/EmbraceStorageInternal/Records/SpanRecord.swift b/Sources/EmbraceStorageInternal/Records/SpanRecord.swift index 03b8fd5f..752d1844 100644 --- a/Sources/EmbraceStorageInternal/Records/SpanRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/SpanRecord.swift @@ -4,20 +4,29 @@ import Foundation import EmbraceCommonInternal -import GRDB +import CoreData /// Represents a span in the storage -public struct SpanRecord: Codable { - public var id: String - public var name: String - public var traceId: String - public var type: SpanType - public var data: Data - public var startTime: Date - public var endTime: Date? - public var processIdentifier: ProcessIdentifier - - public init( +public class SpanRecord: NSManagedObject { + @NSManaged public var id: String + @NSManaged public var name: String + @NSManaged public var traceId: String + @NSManaged public var typeRaw: String // SpanType + @NSManaged public var data: Data + @NSManaged public var startTime: Date + @NSManaged public var endTime: Date? + @NSManaged public var processIdRaw: String // ProcessIdentifier + + public var type: SpanType? { + return SpanType(rawValue: typeRaw) + } + + public var processId: ProcessIdentifier? { + return ProcessIdentifier(hex: processIdRaw) + } + + class func create( + context: NSManagedObjectContext, id: String, name: String, traceId: String, @@ -25,46 +34,77 @@ public struct SpanRecord: Codable { data: Data, startTime: Date, endTime: Date? = nil, - processIdentifier: ProcessIdentifier = .current - ) { - self.id = id - self.traceId = traceId - self.type = type - self.data = data - self.startTime = startTime - self.endTime = endTime - self.name = name - self.processIdentifier = processIdentifier + processIdentifier: ProcessIdentifier + ) -> SpanRecord { + let record = SpanRecord(context: context) + record.id = id + record.name = name + record.traceId = traceId + record.typeRaw = type.rawValue + record.data = data + record.startTime = startTime + record.endTime = endTime + record.processIdRaw = processIdentifier.hex + + return record } -} -extension SpanRecord { - struct Schema { - static var id: Column { Column("id") } - static var traceId: Column { Column("trace_id") } - static var type: Column { Column("type") } - static var data: Column { Column("data") } - static var startTime: Column { Column("start_time") } - static var endTime: Column { Column("end_time") } - static var name: Column { Column("name") } - static var processIdentifier: Column { Column("process_identifier") } + static func createFetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: entityName) } } -extension SpanRecord: FetchableRecord, PersistableRecord, MutablePersistableRecord { - public static let databaseTableName: String = "spans" +extension SpanRecord: EmbraceStorageRecord { + public static var entityName = "SpanRecord" - public static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase - public static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase - public static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .replace, update: .replace) -} + static public var entityDescription: NSEntityDescription { + let entity = NSEntityDescription() + entity.name = entityName + entity.managedObjectClassName = NSStringFromClass(SpanRecord.self) + + let idAttribute = NSAttributeDescription() + idAttribute.name = "id" + idAttribute.attributeType = .stringAttributeType + + let nameAttribute = NSAttributeDescription() + nameAttribute.name = "name" + nameAttribute.attributeType = .stringAttributeType + + let traceIdAttribute = NSAttributeDescription() + traceIdAttribute.name = "traceId" + traceIdAttribute.attributeType = .stringAttributeType + + let typeAttribute = NSAttributeDescription() + typeAttribute.name = "typeRaw" + typeAttribute.attributeType = .stringAttributeType + + let dataAttribute = NSAttributeDescription() + dataAttribute.name = "data" + dataAttribute.attributeType = .binaryDataAttributeType + + let startTimeAttribute = NSAttributeDescription() + startTimeAttribute.name = "startTime" + startTimeAttribute.attributeType = .dateAttributeType + + let endTimeAttribute = NSAttributeDescription() + endTimeAttribute.name = "endTime" + endTimeAttribute.attributeType = .dateAttributeType + + let processIdAttribute = NSAttributeDescription() + processIdAttribute.name = "processIdRaw" + processIdAttribute.attributeType = .stringAttributeType + + entity.properties = [ + idAttribute, + nameAttribute, + traceIdAttribute, + typeAttribute, + dataAttribute, + startTimeAttribute, + endTimeAttribute, + processIdAttribute + ] -extension SpanRecord: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - return - lhs.id == rhs.id && - lhs.traceId == rhs.traceId && - lhs.type == rhs.type && - lhs.data == rhs.data + return entity } } diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests+Async.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests+Async.swift deleted file mode 100644 index 55105020..00000000 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests+Async.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest -import TestSupport -@testable import EmbraceStorageInternal - -extension EmbraceStorageTests { - - func test_updateAsync() throws { - // given inserted record - var span = SpanRecord( - id: "id", - name: "a name", - traceId: "traceId", - type: .performance, - data: Data(), - startTime: Date() - ) - - try storage.dbQueue.write { db in - try span.insert(db) - } - - // then record should exist in storage - let expectation1 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try span.exists(db)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: .defaultTimeout) - - // when updating record - let endTime = Date(timeInterval: 10, since: span.startTime) - span.endTime = endTime - - let expectation2 = XCTestExpectation() - storage.updateAsync(record: span, completion: { result in - switch result { - case .success: - expectation2.fulfill() - case .failure(let error): - XCTAssert(false, error.localizedDescription) - } - }) - - wait(for: [expectation2], timeout: .defaultTimeout) - - // the record should update successfully - try storage.dbQueue.read { db in - XCTAssert(try span.exists(db)) - XCTAssertNotNil(span.endTime) - XCTAssertEqual(span.endTime, endTime) - } - } - - func test_deleteAsync() throws { - // given inserted record - let span = SpanRecord( - id: "id", - name: "a name", - traceId: "traceId", - type: .performance, - data: Data(), - startTime: Date() - ) - - try storage.dbQueue.write { db in - try span.insert(db) - } - - // then record should exist in storage - let expectation1 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try span.exists(db)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: .defaultTimeout) - - // when deleting record - let expectation2 = XCTestExpectation() - storage.deleteAsync(record: span) { result in - switch result { - case .success: - expectation2.fulfill() - case .failure(let error): - XCTAssert(false, error.localizedDescription) - } - } - - wait(for: [expectation2], timeout: .defaultTimeout) - - // the record should update successfully - try storage.dbQueue.read { db in - XCTAssertFalse(try span.exists(db)) - } - } - - func test_fetchAllAsync() throws { - // given inserted records - let span1 = SpanRecord( - id: "id1", - name: "a name 1", - traceId: "traceId", - type: .performance, - data: Data(), - startTime: Date() - ) - let span2 = SpanRecord( - id: "id2", - name: "a name 2", - traceId: "traceId", - type: .performance, - data: Data(), - startTime: Date() - ) - - try storage.dbQueue.write { db in - try span1.insert(db) - try span2.insert(db) - } - - // then records should exist in storage - let expectation1 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try span1.exists(db)) - XCTAssert(try span2.exists(db)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: .defaultTimeout) - - // when fetching all records - storage.fetchAllAsync { (result: Result<[SpanRecord], Error>) in - switch result { - case .success(let records): - // then all records should be successfully fetched - XCTAssert(records.count == 2) - XCTAssert(records.contains(span1)) - XCTAssert(records.contains(span2)) - - case .failure(let error): - XCTAssert(false, error.localizedDescription) - } - } - } - - func test_executeQueryAsync() throws { - // given inserted record - let span = SpanRecord( - id: "id", - name: "a name", - traceId: "traceId", - type: .performance, - data: Data(), - startTime: Date() - ) - - try storage.dbQueue.write { db in - try span.insert(db) - } - - // then record should exist in storage - let expectation1 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try span.exists(db)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: .defaultTimeout) - - // when executing custom query to delete record - let expectation2 = XCTestExpectation() - storage.executeQueryAsync("DELETE FROM \(SpanRecord.databaseTableName) WHERE id='id' AND trace_id='traceId'", arguments: nil) { result in - - switch result { - case .success: - expectation2.fulfill() - case .failure(let error): - XCTAssert(false, error.localizedDescription) - } - } - - wait(for: [expectation2], timeout: .defaultTimeout) - - // then record should not exist in storage - let expectation3 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertFalse(try span.exists(db)) - expectation3.fulfill() - } - - wait(for: [expectation3], timeout: .defaultTimeout) - } -} diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift index 22f4486a..01766c42 100644 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift +++ b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift @@ -228,43 +228,6 @@ class EmbraceStorageTests: XCTestCase { XCTAssert(records.contains(span2)) } - func test_executeQuery() throws { - // given inserted record - let span = SpanRecord( - id: "id", - name: "a name", - traceId: "traceId", - type: .performance, - data: Data(), - startTime: Date() - ) - - try storage.dbQueue.write { db in - try span.insert(db) - } - - // then record should exist in storage - let expectation1 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try span.exists(db)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: .defaultTimeout) - - // when executing custom query to delete record - try storage.executeQuery("DELETE FROM \(SpanRecord.databaseTableName) WHERE id='id' AND trace_id='traceId'", arguments: nil) - - // then record should not exist in storage - let expectation2 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertFalse(try span.exists(db)) - expectation2.fulfill() - } - - wait(for: [expectation2], timeout: .defaultTimeout) - } - func test_corruptedDbAction() { guard let corruptedDb = EmbraceStorageTests.prepareCorruptedDBForTest() else { return XCTFail("\(#function): Failed to create corrupted DB for test") From 01ed960b93ebe7b78f74c2033be91893a835ac4b Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Wed, 29 Jan 2025 13:36:42 -0300 Subject: [PATCH 09/17] WIP --- EmbraceIO.podspec | 6 - Package.resolved | 9 - Package.swift | 17 +- Project.swift | 6 +- .../EmbraceCore/Capture/CaptureServices.swift | 2 +- .../Capture/ResourceCaptureService.swift | 12 +- Sources/EmbraceCore/Embrace.swift | 2 +- .../DeviceIdentifier+Persistence.swift | 2 +- .../Logs/EmbraceLogAttributesBuilder.swift | 6 +- .../Logs/Exporter/DefaultLogBatcher.swift | 24 +-- .../Exporter/StorageEmbraceLogExporter.swift | 41 +---- .../Internal/Logs/LogController.swift | 10 +- .../ResourceStorageExporter.swift | 2 +- .../Tracing/StorageSpanExporter.swift | 38 ++--- .../EmbraceCore/Payload/AppInfoPayload.swift | 16 +- .../Payload/Builders/LogPayloadBuilder.swift | 2 +- .../Builders/SessionPayloadBuilder.swift | 31 ++-- .../Builders/SpansPayloadBuilder.swift | 9 +- .../EmbraceCore/Payload/MetadataPayload.swift | 10 +- .../EmbraceCore/Payload/ResourcePayload.swift | 48 +++--- .../Metadata/MetadataHandler+User.swift | 15 +- .../Public/Metadata/MetadataHandler.swift | 62 +++---- .../Public/Metadata/MetadataRecordTmp.swift | 12 -- .../DataRecovery/UnsentDataHandler.swift | 30 ++-- .../Session/SessionController.swift | 27 +-- .../Session/SessionSpanUtils.swift | 2 +- .../EmbraceStorage.swift | 29 ++-- .../EmbraceStorageError.swift | 1 - .../Migration/Migration.swift | 27 --- .../Migration/MigrationService.swift | 44 ----- .../20240509_00_AddSpanRecordMigration.swift | 36 ---- ...0240510_00_AddSessionRecordMigration.swift | 39 ----- ...240510_01_AddMetadataRecordMigration.swift | 29 ---- .../20240510_02_AddLogRecordMigration.swift | 21 --- ...ocessIdentifierToSpanRecordMigration.swift | 73 -------- .../Migrations/Migrations+Current.swift | 20 --- .../Records/EmbraceStorage+Log.swift | 63 +++++++ .../Records/EmbraceStorage+Metadata.swift | 13 +- .../Records/EmbraceStorage+Session.swift | 1 - .../Records/EmbraceStorage+Span.swift | 1 - .../Records/EmbraceStorageRecord.swift | 1 - .../Records/Log/EmbraceStorage+Log.swift | 58 ------- .../Records/Log/LogRecord.swift | 84 --------- .../Records/Log/LogRecordRepository.swift | 13 -- .../Records/Log/PersistableValue.swift | 90 ---------- .../Records/LogAttributeRecord.swift | 63 +++++++ .../Records/LogRecord.swift | 159 ++++++++++++++++++ ...eUploadType+DatabaseValueConvertible.swift | 18 -- .../Cache/UploadDataRecord.swift | 1 - bin/build_dependencies.sh | 2 +- bin/dependencies/build_grdb.swift.sh | 7 - bin/templates/EmbraceIO.podspec.tpl | 6 - 52 files changed, 478 insertions(+), 862 deletions(-) delete mode 100644 Sources/EmbraceStorageInternal/Migration/Migration.swift delete mode 100644 Sources/EmbraceStorageInternal/Migration/MigrationService.swift delete mode 100644 Sources/EmbraceStorageInternal/Migration/Migrations/20240509_00_AddSpanRecordMigration.swift delete mode 100644 Sources/EmbraceStorageInternal/Migration/Migrations/20240510_00_AddSessionRecordMigration.swift delete mode 100644 Sources/EmbraceStorageInternal/Migration/Migrations/20240510_01_AddMetadataRecordMigration.swift delete mode 100644 Sources/EmbraceStorageInternal/Migration/Migrations/20240510_02_AddLogRecordMigration.swift delete mode 100644 Sources/EmbraceStorageInternal/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigration.swift delete mode 100644 Sources/EmbraceStorageInternal/Migration/Migrations/Migrations+Current.swift create mode 100644 Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift delete mode 100644 Sources/EmbraceStorageInternal/Records/Log/EmbraceStorage+Log.swift delete mode 100644 Sources/EmbraceStorageInternal/Records/Log/LogRecord.swift delete mode 100644 Sources/EmbraceStorageInternal/Records/Log/LogRecordRepository.swift delete mode 100644 Sources/EmbraceStorageInternal/Records/Log/PersistableValue.swift create mode 100644 Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift create mode 100644 Sources/EmbraceStorageInternal/Records/LogRecord.swift delete mode 100644 Sources/EmbraceUploadInternal/Cache/EmbraceUploadType+DatabaseValueConvertible.swift delete mode 100644 bin/dependencies/build_grdb.swift.sh diff --git a/EmbraceIO.podspec b/EmbraceIO.podspec index a46f9199..58d0ace7 100644 --- a/EmbraceIO.podspec +++ b/EmbraceIO.podspec @@ -75,14 +75,12 @@ Pod::Spec.new do |spec| storage.vendored_frameworks = "xcframeworks/EmbraceStorageInternal.xcframework" storage.dependency "EmbraceIO/EmbraceCommonInternal" storage.dependency "EmbraceIO/EmbraceSemantics" - storage.dependency "EmbraceIO/GRDB" end spec.subspec 'EmbraceUploadInternal' do |upload| upload.vendored_frameworks = "xcframeworks/EmbraceUploadInternal.xcframework" upload.dependency "EmbraceIO/EmbraceCommonInternal" upload.dependency "EmbraceIO/EmbraceOTelInternal" - upload.dependency "EmbraceIO/GRDB" end spec.subspec 'EmbraceCrashlyticsSupport' do |cs| @@ -110,10 +108,6 @@ Pod::Spec.new do |spec| otelSdk.dependency "EmbraceIO/OpenTelemetryApi" end - spec.subspec 'GRDB' do |grdb| - grdb.vendored_frameworks = "xcframeworks/GRDB.xcframework" - end - spec.subspec 'KSCrash' do |kscrash| kscrash.dependency "EmbraceIO/KSCrashCore" kscrash.dependency "EmbraceIO/KSCrashRecording" diff --git a/Package.resolved b/Package.resolved index 876c3aef..a0639290 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "grdb.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDB.swift.git", - "state" : { - "revision" : "dd6b98ce04eda39aa22f066cd421c24d7236ea8a", - "version" : "6.29.1" - } - }, { "identity" : "grpc-swift", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 6a321f3c..cbd42c42 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,6 @@ import PackageDescription let packageSettings = PackageSettings( productTypes: [ - "GRDB": .framework, "KSCrash": .framework, "OpenTelemetrySdk": .framework, "OpenTelemetryApi": .framework @@ -37,10 +36,6 @@ let package = Package( .package( url: "https://github.com/open-telemetry/opentelemetry-swift", exact: "1.12.1" - ), - .package( - url: "https://github.com/groue/GRDB.swift", - .upToNextMinor(from: "6.29.1") ) ], targets: [ @@ -62,8 +57,7 @@ let package = Package( "EmbraceIO", "EmbraceCore", "EmbraceCrash", - "TestSupport", - .product(name: "GRDB", package: "GRDB.swift") + "TestSupport" ] ), @@ -91,8 +85,7 @@ let package = Package( dependencies: [ "EmbraceCore", "TestSupport", - "TestSupportObjc", - .product(name: "GRDB", package: "GRDB.swift") + "TestSupportObjc" ], resources: [ .copy("Mocks/") @@ -190,8 +183,7 @@ let package = Package( name: "EmbraceStorageInternal", dependencies: [ "EmbraceCommonInternal", - "EmbraceSemantics", - .product(name: "GRDB", package: "GRDB.swift") + "EmbraceSemantics" ] ), .testTarget( @@ -208,8 +200,7 @@ let package = Package( dependencies: [ "EmbraceCommonInternal", "EmbraceOTelInternal", - "EmbraceCoreDataInternal", - .product(name: "GRDB", package: "GRDB.swift") + "EmbraceCoreDataInternal" ] ), .testTarget( diff --git a/Project.swift b/Project.swift index dccd28a6..c8c68899 100644 --- a/Project.swift +++ b/Project.swift @@ -151,8 +151,7 @@ let project = Project( dependencies: [ .target(name: "EmbraceCommonInternal"), .target(name: "EmbraceSemantics"), - .external(name: "OpenTelemetryApi"), - .external(name: "GRDB") + .external(name: "OpenTelemetryApi") ], settings: .settings(base: [ "SKIP_INSTALL": "NO", @@ -168,8 +167,7 @@ let project = Project( sources: ["Sources/EmbraceUploadInternal/**"], dependencies: [ .target(name: "EmbraceCommonInternal"), - .target(name: "EmbraceOTelInternal"), - .external(name: "GRDB") + .target(name: "EmbraceOTelInternal") ], settings: .settings(base: [ "SKIP_INSTALL": "NO", diff --git a/Sources/EmbraceCore/Capture/CaptureServices.swift b/Sources/EmbraceCore/Capture/CaptureServices.swift index 6c800b97..dca696a0 100644 --- a/Sources/EmbraceCore/Capture/CaptureServices.swift +++ b/Sources/EmbraceCore/Capture/CaptureServices.swift @@ -104,7 +104,7 @@ final class CaptureServices { @objc func onSessionStart(notification: Notification) { if let session = notification.object as? SessionRecord { - crashReporter?.currentSessionId = session.id.toString + crashReporter?.currentSessionId = session.idRaw } } } diff --git a/Sources/EmbraceCore/Capture/ResourceCaptureService.swift b/Sources/EmbraceCore/Capture/ResourceCaptureService.swift index 997053c4..0fd34720 100644 --- a/Sources/EmbraceCore/Capture/ResourceCaptureService.swift +++ b/Sources/EmbraceCore/Capture/ResourceCaptureService.swift @@ -23,13 +23,11 @@ extension EmbraceStorage: ResourceCaptureServiceHandler { func addResource(key: String, value: AttributeValue) { do { _ = try addMetadata( - MetadataRecord( - key: key, - value: value, - type: .requiredResource, - lifespan: .process, - lifespanId: ProcessIdentifier.current.hex - ) + key: key, + value: value.description, + type: .requiredResource, + lifespan: .process, + lifespanId: ProcessIdentifier.current.hex ) } catch { Embrace.logger.error("Failed to capture resource: \(error.localizedDescription)") diff --git a/Sources/EmbraceCore/Embrace.swift b/Sources/EmbraceCore/Embrace.swift index d8e5f6a8..341bee5d 100644 --- a/Sources/EmbraceCore/Embrace.swift +++ b/Sources/EmbraceCore/Embrace.swift @@ -298,7 +298,7 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta return nil } - return sessionController.currentSession?.id.toString + return sessionController.currentSession?.idRaw } /// Returns the current device identifier. diff --git a/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift b/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift index a608277e..cd96ce69 100644 --- a/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift +++ b/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift @@ -14,7 +14,7 @@ extension DeviceIdentifier { if let storage = storage { do { if let resource = try storage.fetchRequiredPermanentResource(key: resourceKey) { - if let uuid = resource.uuidValue { + if let uuid = UUID(withoutHyphen: resource.value) { return DeviceIdentifier(value: uuid) } diff --git a/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift b/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift index fcbd1e33..52bd6537 100644 --- a/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift +++ b/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift @@ -77,10 +77,8 @@ class EmbraceLogAttributesBuilder { return } - if let value = record.stringValue { - let key = String(format: LogSemantics.keyPropertiesPrefix, record.key) - attributes[key] = value - } + let key = String(format: LogSemantics.keyPropertiesPrefix, record.key) + attributes[key] = record.value } } return self diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift index 3637d1c5..794649e6 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift @@ -3,16 +3,17 @@ // import Foundation - import EmbraceStorageInternal import EmbraceCommonInternal +import OpenTelemetryApi +import OpenTelemetrySdk protocol LogBatcherDelegate: AnyObject { func batchFinished(withLogs logs: [LogRecord]) } protocol LogBatcher { - func addLogRecord(logRecord: LogRecord) + func addLogRecord(logRecord: ReadableLogRecord) func renewBatch(withLogs logRecords: [LogRecord]) func forceEndCurrentBatch() } @@ -38,16 +39,17 @@ class DefaultLogBatcher: LogBatcher { self.delegate = delegate } - func addLogRecord(logRecord: LogRecord) { + func addLogRecord(logRecord: ReadableLogRecord) { processorQueue.async { - self.repository.create(logRecord) { result in - switch result { - case .success: - self.addLogToBatch(logRecord) - case .failure(let error): - Embrace.logger.error(error.localizedDescription) - } - } + let record = self.repository.create( + id: LogIdentifier(), + processId: ProcessIdentifier.current, + severity: logRecord.severity?.toLogSeverity() ?? .info, + body: logRecord.body?.description ?? "", + timestamp: logRecord.timestamp, + attributes: logRecord.attributes + ) + self.addLogToBatch(record) } } } diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/StorageEmbraceLogExporter.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/StorageEmbraceLogExporter.swift index d9366437..eabfc9ee 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/StorageEmbraceLogExporter.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/StorageEmbraceLogExporter.swift @@ -56,7 +56,7 @@ class StorageEmbraceLogExporter: LogRecordExporter { continue } - self.logBatcher.addLogRecord(logRecord: buildLogRecord(from: log)) + self.logBatcher.addLogRecord(logRecord: log) } return .success @@ -72,42 +72,3 @@ class StorageEmbraceLogExporter: LogRecordExporter { .success } } - -private extension StorageEmbraceLogExporter { - func buildLogRecord(from originalLog: ReadableLogRecord) -> LogRecord { - let embAttributes = originalLog.attributes.reduce(into: [String: PersistableValue]()) { - $0[$1.key] = PersistableValue(attributeValue: $1.value) - } - return .init(identifier: LogIdentifier(), - processIdentifier: ProcessIdentifier.current, - severity: originalLog.severity?.toLogSeverity() ?? .info, - body: originalLog.body?.description ?? "", - attributes: embAttributes, - timestamp: originalLog.timestamp) - } -} - -private extension PersistableValue { - init?(attributeValue: AttributeValue) { - switch attributeValue { - case let .string(value): - self.init(value) - case let .bool(value): - self.init(value) - case let .int(value): - self.init(value) - case let .double(value): - self.init(value) - case let .stringArray(value): - self.init(value) - case let .boolArray(value): - self.init(value) - case let .intArray(value): - self.init(value) - case let .doubleArray(value): - self.init(value) - default: - return nil - } - } -} diff --git a/Sources/EmbraceCore/Internal/Logs/LogController.swift b/Sources/EmbraceCore/Internal/Logs/LogController.swift index 34c2fcd9..43872823 100644 --- a/Sources/EmbraceCore/Internal/Logs/LogController.swift +++ b/Sources/EmbraceCore/Internal/Logs/LogController.swift @@ -181,6 +181,10 @@ private extension LogController { continue } + guard let processId = batch.logs[0].processId else { + return + } + // Since we always end batches when a session ends // all the logs still in storage when the app starts should come // from the last session before the app closes. @@ -191,12 +195,10 @@ private extension LogController { // If we can't find a sessionId, we use the processId instead var sessionId: SessionIdentifier? - if let log = batch.logs.first(where: { $0.attributes[LogSemantics.keySessionId] != nil }) { - sessionId = SessionIdentifier(string: log.attributes[LogSemantics.keySessionId]?.description) + if let log = batch.logs.first(where: { $0.attribute(forKey: LogSemantics.keySessionId) != nil }) { + sessionId = SessionIdentifier(string: log.attribute(forKey: LogSemantics.keySessionId)?.valueRaw) } - let processId = batch.logs[0].processIdentifier - let resourcePayload = try createResourcePayload(sessionId: sessionId, processId: processId) let metadataPayload = try createMetadataPayload(sessionId: sessionId, processId: processId) diff --git a/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift b/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift index d6b46d17..8dbbdaf6 100644 --- a/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift +++ b/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift @@ -35,7 +35,7 @@ class ResourceStorageExporter: EmbraceResourceProvider { } var attributes: [String: AttributeValue] = records.reduce(into: [:]) { partialResult, record in - partialResult[record.key] = record.value + partialResult[record.key] = .string(record.value) } if attributes[ResourceAttributes.serviceName.rawValue] == nil { diff --git a/Sources/EmbraceCore/Internal/Tracing/StorageSpanExporter.swift b/Sources/EmbraceCore/Internal/Tracing/StorageSpanExporter.swift index be122172..c8afde4b 100644 --- a/Sources/EmbraceCore/Internal/Tracing/StorageSpanExporter.swift +++ b/Sources/EmbraceCore/Internal/Tracing/StorageSpanExporter.swift @@ -29,10 +29,22 @@ class StorageSpanExporter: SpanExporter { var result = SpanExporterResultCode.success for var spanData in spans { - let isValid = validation.execute(spanData: &spanData) - if isValid, let record = buildRecord(from: spanData) { + if validation.execute(spanData: &spanData) { do { - try storage.upsertSpan(record) + let data = try spanData.toJSON() + + // spanData endTime is non-optional and will be set during `toSpanData()` + let endTime = spanData.hasEnded ? spanData.endTime : nil + + storage.upsertSpan( + id: spanData.spanId.hexString, + name: spanData.name, + traceId: spanData.traceId.hexString, + type: spanData.embType, + data: data, + startTime: spanData.startTime, + endTime: endTime + ) } catch let exception { self.logger?.error(exception.localizedDescription) result = .failure @@ -55,23 +67,3 @@ class StorageSpanExporter: SpanExporter { } } - -extension StorageSpanExporter { - private func buildRecord(from spanData: SpanData) -> SpanRecord? { - guard let data = try? spanData.toJSON() else { - return nil - } - - // spanData endTime is non-optional and will be set during `toSpanData()` - let endTime = spanData.hasEnded ? spanData.endTime : nil - - return SpanRecord( - id: spanData.spanId.hexString, - name: spanData.name, - traceId: spanData.traceId.hexString, - type: spanData.embType, - data: data, - startTime: spanData.startTime, - endTime: endTime ) - } -} diff --git a/Sources/EmbraceCore/Payload/AppInfoPayload.swift b/Sources/EmbraceCore/Payload/AppInfoPayload.swift index 4c909235..14ee4fe7 100644 --- a/Sources/EmbraceCore/Payload/AppInfoPayload.swift +++ b/Sources/EmbraceCore/Payload/AppInfoPayload.swift @@ -40,21 +40,21 @@ struct AppInfoPayload: Codable { switch key { case .bundleVersion: - self.bundleVersion = resource.stringValue + self.bundleVersion = resource.value case .environment: - self.environment = resource.stringValue + self.environment = resource.value case .detailedEnvironment: - self.detailedEnvironment = resource.stringValue + self.detailedEnvironment = resource.value case .framework: - self.framework = resource.integerValue + self.framework = Int(resource.value) case .launchCount: - self.launchCount = resource.integerValue + self.launchCount = Int(resource.value) case .sdkVersion: - self.sdkVersion = resource.stringValue + self.sdkVersion = resource.value case .appVersion: - self.appVersion = resource.stringValue + self.appVersion = resource.value case .buildID: - self.buildID = resource.stringValue + self.buildID = resource.value default: break } } diff --git a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift index b3243524..18ba568b 100644 --- a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift @@ -13,7 +13,7 @@ struct LogPayloadBuilder { Attribute(key: entry.key, value: entry.value.description) } - finalAttributes.append(.init(key: LogSemantics.keyId, value: log.identifier.toString)) + finalAttributes.append(.init(key: LogSemantics.keyId, value: log.idRaw)) return .init(timeUnixNano: String(Int(log.timestamp.nanosecondsSince1970)), severityNumber: log.severity.number, diff --git a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift index 95f1a47e..bb80a663 100644 --- a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift @@ -10,7 +10,11 @@ class SessionPayloadBuilder { static var resourceName = "emb.session.upload_index" - class func build(for sessionRecord: SessionRecord, storage: EmbraceStorage) -> PayloadEnvelope<[SpanPayload]> { + class func build(for sessionRecord: SessionRecord, storage: EmbraceStorage) -> PayloadEnvelope<[SpanPayload]>? { + guard let sessionId = sessionRecord.id else { + return nil + } + var resource: MetadataRecord? do { @@ -25,9 +29,9 @@ class SessionPayloadBuilder { do { if var resource = resource { - counter = (resource.integerValue ?? 0) + 1 - resource.value = .string(String(counter)) - try storage.updateMetadata(resource) + counter = (Int(resource.value) ?? 0) + 1 + resource.value = String(counter) + storage.save() } else { resource = try storage.addMetadata( key: resourceName, @@ -49,24 +53,15 @@ class SessionPayloadBuilder { ) // build resources payload - var resources: [MetadataRecord] = [] - do { - resources = try storage.fetchResourcesForSessionId(sessionRecord.id) - } catch { - Embrace.logger.error("Error fetching resources for session \(sessionRecord.id.toString)") - } + let resources: [MetadataRecord] = storage.fetchResourcesForSessionId(sessionId) let resourcePayload = ResourcePayload(from: resources) // build metadata payload var metadata: [MetadataRecord] = [] - do { - let properties = try storage.fetchCustomPropertiesForSessionId(sessionRecord.id) - let tags = try storage.fetchPersonaTagsForSessionId(sessionRecord.id) - metadata.append(contentsOf: properties) - metadata.append(contentsOf: tags) - } catch { - Embrace.logger.error("Error fetching custom properties for session \(sessionRecord.id.toString)") - } + let properties = storage.fetchCustomPropertiesForSessionId(sessionId) + let tags = storage.fetchPersonaTagsForSessionId(sessionId) + metadata.append(contentsOf: properties) + metadata.append(contentsOf: tags) let metadataPayload = MetadataPayload(from: metadata) // build payload diff --git a/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift index 4fef2903..acd32ac1 100644 --- a/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift @@ -76,13 +76,16 @@ class SpansPayloadBuilder { ) -> SpanPayload? { do { var spanData: SpanData? - let sessionSpan = try storage.fetchSpan(id: sessionRecord.spanId, traceId: sessionRecord.traceId) + let sessionSpan = storage.fetchSpan(id: sessionRecord.spanId, traceId: sessionRecord.traceId) if let rawData = sessionSpan?.data { spanData = try JSONDecoder().decode(SpanData.self, from: rawData) } - let properties = try storage.fetchCustomPropertiesForSessionId(sessionRecord.id) + var properties: [MetadataRecord] = [] + if let sessionId = sessionRecord.id { + properties = storage.fetchCustomPropertiesForSessionId(sessionId) + } return SessionSpanUtils.payload( from: sessionRecord, @@ -92,7 +95,7 @@ class SpansPayloadBuilder { ) } catch { - Embrace.logger.warning("Error fetching span for session \(sessionRecord.id):\n\(error.localizedDescription)") + Embrace.logger.warning("Error fetching span for session \(sessionRecord.idRaw):\n\(error.localizedDescription)") } return nil diff --git a/Sources/EmbraceCore/Payload/MetadataPayload.swift b/Sources/EmbraceCore/Payload/MetadataPayload.swift index d62edbe5..26d29667 100644 --- a/Sources/EmbraceCore/Payload/MetadataPayload.swift +++ b/Sources/EmbraceCore/Payload/MetadataPayload.swift @@ -24,20 +24,20 @@ struct MetadataPayload: Codable { if let key = UserResourceKey(rawValue: record.key) { switch key { case .name: - self.username = record.stringValue + self.username = record.value case .email: - self.email = record.stringValue + self.email = record.value case .identifier: - self.userId = record.stringValue + self.userId = record.value } } if let key = DeviceResourceKey(rawValue: record.key) { switch key { case .locale: - self.locale = record.stringValue + self.locale = record.value case .timezone: - self.timezoneDescription = record.stringValue + self.timezoneDescription = record.value default: break } diff --git a/Sources/EmbraceCore/Payload/ResourcePayload.swift b/Sources/EmbraceCore/Payload/ResourcePayload.swift index d270f056..c2b5c94c 100644 --- a/Sources/EmbraceCore/Payload/ResourcePayload.swift +++ b/Sources/EmbraceCore/Payload/ResourcePayload.swift @@ -116,63 +116,63 @@ struct ResourcePayload: Codable { if let key = AppResourceKey(rawValue: resource.key) { switch key { case .bundleVersion: - self.bundleVersion = resource.stringValue + self.bundleVersion = resource.value case .environment: - self.environment = resource.stringValue + self.environment = resource.value case .detailedEnvironment: - self.environmentDetail = resource.stringValue + self.environmentDetail = resource.value case .framework: - self.appFramework = resource.integerValue + self.appFramework = Int(resource.value) case .launchCount: - self.launchCount = resource.integerValue + self.launchCount = Int(resource.value) case .sdkVersion: - self.sdkVersion = resource.stringValue + self.sdkVersion = resource.value case .appVersion: - self.appVersion = resource.stringValue + self.appVersion = resource.value case .processIdentifier: - self.processIdentifier = resource.stringValue + self.processIdentifier = resource.value case .buildID: - self.buildId = resource.stringValue + self.buildId = resource.value case .processStartTime: - self.processStartTime = resource.integerValue + self.processStartTime = Int(resource.value) case .processPreWarm: - self.processPreWarm = resource.boolValue + self.processPreWarm = Bool(resource.value) } } else if let key = DeviceResourceKey(rawValue: resource.key) { switch key { case .isJailbroken: - self.jailbroken = resource.boolValue + self.jailbroken = Bool(resource.value) case .totalDiskSpace: - self.diskTotalCapacity = resource.integerValue + self.diskTotalCapacity = Int(resource.value) case .architecture: - self.deviceArchitecture = resource.stringValue + self.deviceArchitecture = resource.value case .screenResolution: - self.screenResolution = resource.stringValue + self.screenResolution = resource.value case .osBuild: - self.osBuild = resource.stringValue + self.osBuild = resource.value case .osVariant: - self.osAlternateType = resource.stringValue + self.osAlternateType = resource.value default: break } } else if let key = ResourceAttributes(rawValue: resource.key) { switch key { case .deviceModelIdentifier: - self.deviceModel = resource.stringValue + self.deviceModel = resource.value case .deviceManufacturer: - self.deviceManufacturer = resource.stringValue + self.deviceManufacturer = resource.value case .osVersion: - self.osVersion = resource.stringValue + self.osVersion = resource.value case .osType: - self.osType = resource.stringValue + self.osType = resource.value case .osName: - self.osName = resource.stringValue + self.osName = resource.value default: break } - } else if let value = resource.stringValue { - self.additionalResources[resource.key] = value + } else { + self.additionalResources[resource.key] = resource.value } } } diff --git a/Sources/EmbraceCore/Public/Metadata/MetadataHandler+User.swift b/Sources/EmbraceCore/Public/Metadata/MetadataHandler+User.swift index e0f8b912..cb612da4 100644 --- a/Sources/EmbraceCore/Public/Metadata/MetadataHandler+User.swift +++ b/Sources/EmbraceCore/Public/Metadata/MetadataHandler+User.swift @@ -50,24 +50,15 @@ import EmbraceStorageInternal /// Clear all user properties. /// This will clear all user properties set via the `userName`, `userEmail` and `userIdentifier` properties. public func clearUserProperties() { - do { - try storage?.removeAllMetadata(keys: UserResourceKey.allValues, lifespan: .permanent) - } catch { - Embrace.logger.warning("Unable to clear user metadata") - } + storage?.removeAllMetadata(keys: UserResourceKey.allValues, lifespan: .permanent) } } extension MetadataHandler { private func value(for key: UserResourceKey) -> String? { - do { - let record = try storage?.fetchMetadata(key: key.rawValue, type: .customProperty, lifespan: .permanent) - return record?.stringValue - } catch { - Embrace.logger.warning("Unable to read user metadata!") - } - return nil + let record = storage?.fetchMetadata(key: key.rawValue, type: .customProperty, lifespan: .permanent) + return record?.value } private func update(key: UserResourceKey, value: String?) { diff --git a/Sources/EmbraceCore/Public/Metadata/MetadataHandler.swift b/Sources/EmbraceCore/Public/Metadata/MetadataHandler.swift index 0bc6bc2a..49faa116 100644 --- a/Sources/EmbraceCore/Public/Metadata/MetadataHandler.swift +++ b/Sources/EmbraceCore/Public/Metadata/MetadataHandler.swift @@ -39,7 +39,7 @@ public class MetadataHandler: NSObject { var storageMechanism: StorageMechanism = .inMemory(name: coreDataStackName) // in memory only used for tests if let storage = storage, - let url = storage.options.baseUrl { + let url = storage.options.storageMechanism.baseUrl { storageMechanism = .onDisk(name: coreDataStackName, baseURL: url) } @@ -213,7 +213,7 @@ extension MetadataHandler { extension MetadataHandler { private func currentContext(for lifespan: MetadataRecordLifespan) throws -> String { if lifespan == .session { - guard let sessionId = sessionController?.currentSession?.id.toString else { + guard let sessionId = sessionController?.currentSession?.id?.toString else { throw MetadataError.invalidSession("Can't add a session property if there's no active session!") } return sessionId @@ -239,34 +239,34 @@ extension MetadataLifespan { // tmp core data stack extension MetadataHandler { func cloneDataBase() { - guard let coreData = coreData, - let storage = storage else { - return - } - - let request = NSFetchRequest(entityName: MetadataRecordTmp.entityName) - let oldRecords = coreData.fetch(withRequest: request) - coreData.deleteRecords(oldRecords) - - do { - var newRecords: [MetadataRecord] = [] - try storage.dbQueue.read { db in - newRecords = try MetadataRecord.fetchAll(db) - } - - coreData.context.perform { - for record in newRecords { - _ = MetadataRecordTmp.create(context: coreData.context, record: record) - } - - do { - try coreData.context.save() - } catch { - Embrace.logger.error("Error saving metadata core data!:\n\(error.localizedDescription)") - } - } - } catch { - Embrace.logger.error("Error cloning metadata!:\n\(error.localizedDescription)") - } +// guard let coreData = coreData, +// let storage = storage else { +// return +// } +// +// let request = NSFetchRequest(entityName: MetadataRecordTmp.entityName) +// let oldRecords = coreData.fetch(withRequest: request) +// coreData.deleteRecords(oldRecords) +// +// do { +// var newRecords: [MetadataRecord] = [] +// try storage.dbQueue.read { db in +// newRecords = try MetadataRecord.fetchAll(db) +// } +// +// coreData.context.perform { +// for record in newRecords { +// _ = MetadataRecordTmp.create(context: coreData.context, record: record) +// } +// +// do { +// try coreData.context.save() +// } catch { +// Embrace.logger.error("Error saving metadata core data!:\n\(error.localizedDescription)") +// } +// } +// } catch { +// Embrace.logger.error("Error cloning metadata!:\n\(error.localizedDescription)") +// } } } diff --git a/Sources/EmbraceCore/Public/Metadata/MetadataRecordTmp.swift b/Sources/EmbraceCore/Public/Metadata/MetadataRecordTmp.swift index 75da1d8e..2a9aef9e 100644 --- a/Sources/EmbraceCore/Public/Metadata/MetadataRecordTmp.swift +++ b/Sources/EmbraceCore/Public/Metadata/MetadataRecordTmp.swift @@ -34,18 +34,6 @@ public class MetadataRecordTmp: NSManagedObject { return record } - - class func create(context: NSManagedObjectContext, record: MetadataRecord) -> MetadataRecordTmp { - return create( - context: context, - key: record.key, - value: record.value.description, - type: record.type.rawValue, - lifespan: record.lifespan.rawValue, - lifespanId: record.lifespanId, - collectedAt: record.collectedAt - ) - } } extension MetadataRecordTmp { diff --git a/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift b/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift index 9493303c..5119b506 100644 --- a/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift +++ b/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift @@ -53,25 +53,23 @@ class UnsentDataHandler { crashReports: [CrashReport] ) { // send crash reports + var save = false + for report in crashReports { // link session with crash report if possible var session: SessionRecord? if let sessionId = SessionIdentifier(string: report.sessionId) { - do { - session = try storage.fetchSession(id: sessionId) - if var session = session { - // update session's end time with the crash report timestamp - session.endTime = report.timestamp ?? session.endTime + session = storage.fetchSession(id: sessionId) + if let session = session { + // update session's end time with the crash report timestamp + session.endTime = report.timestamp ?? session.endTime - // update crash report id - session.crashReportId = report.id.uuidString + // update crash report id + session.crashReportId = report.id.uuidString - try storage.update(record: session) - } - } catch { - Embrace.logger.warning("Error updating session \(sessionId) with crashReportId \(report.id)!") + save = true } } @@ -86,6 +84,10 @@ class UnsentDataHandler { ) } + if save { + storage.save() + } + // send sessions sendSessions( storage: storage, @@ -229,7 +231,7 @@ class UnsentDataHandler { do { payloadData = try JSONEncoder().encode(payload).gzipped() } catch { - Embrace.logger.warning("Error encoding session \(session.id.toString):\n" + error.localizedDescription) + Embrace.logger.warning("Error encoding session \(session.idRaw):\n" + error.localizedDescription) return } @@ -238,13 +240,13 @@ class UnsentDataHandler { } // upload session spans - upload.uploadSpans(id: session.id.toString, data: payloadData) { result in + upload.uploadSpans(id: session.idRaw, data: payloadData) { result in switch result { case .success: do { // remove session from storage // we can remove this immediately because the upload module will cache it until the upload succeeds - try storage.delete(record: session) + storage.delete(session) if performCleanUp { cleanOldSpans(storage: storage) diff --git a/Sources/EmbraceCore/Session/SessionController.swift b/Sources/EmbraceCore/Session/SessionController.swift index b1d7cd6a..c89baeaa 100644 --- a/Sources/EmbraceCore/Session/SessionController.swift +++ b/Sources/EmbraceCore/Session/SessionController.swift @@ -96,6 +96,10 @@ class SessionController: SessionControllable { return nil } + guard let storage = storage else { + return nil + } + // detect cold start let isColdStart = firstSession @@ -125,7 +129,7 @@ class SessionController: SessionControllable { currentSessionSpan = span // create session record - var session = SessionRecord( + let session = storage.addSession( id: newId, state: state, processId: ProcessIdentifier.current, @@ -239,30 +243,15 @@ class SessionController: SessionControllable { extension SessionController { private func save() { - guard let storage = storage, - let session = currentSession else { - return - } - - do { - try storage.upsertSession(session) - } catch { - Embrace.logger.warning("Error trying to update session:\n\(error.localizedDescription)") - } + storage?.save() } private func delete() { - guard let storage = storage, - let session = currentSession else { + guard let session = currentSession else { return } - do { - try storage.delete(record: session) - } catch { - Embrace.logger.warning("Error trying to delete session:\n\(error.localizedDescription)") - } - + storage?.delete(session) currentSession = nil currentSessionSpan = nil } diff --git a/Sources/EmbraceCore/Session/SessionSpanUtils.swift b/Sources/EmbraceCore/Session/SessionSpanUtils.swift index d83e89a4..45fefd7e 100644 --- a/Sources/EmbraceCore/Session/SessionSpanUtils.swift +++ b/Sources/EmbraceCore/Session/SessionSpanUtils.swift @@ -70,7 +70,7 @@ fileprivate extension SpanPayload { ), Attribute( key: SpanSemantics.Session.keyId, - value: session.id.toString + value: session.idRaw ), Attribute( key: SpanSemantics.Session.keyState, diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage.swift b/Sources/EmbraceStorageInternal/EmbraceStorage.swift index e65f5dd6..c561579c 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage.swift @@ -6,13 +6,12 @@ import Foundation import EmbraceCommonInternal import EmbraceCoreDataInternal import CoreData -import GRDB public typealias Storage = EmbraceStorageMetadataFetcher & LogRepository /// Class in charge of storing all the data captured by the Embrace SDK. -/// It provides an abstraction layer over a GRDB SQLite database. -public class EmbraceStorage: Storage { +/// It provides an abstraction layer over a CoreData database. +public class EmbraceStorage: Storage { public private(set) var options: Options public private(set) var logger: InternalLogger public private(set) var coreData: CoreDataWrapper @@ -31,14 +30,16 @@ public class EmbraceStorage: Storage { } // create core data stack + var entities: [NSEntityDescription] = [ + SessionRecord.entityDescription, + SpanRecord.entityDescription, + MetadataRecord.entityDescription + ] + entities.append(contentsOf: LogRecord.entityDescriptions) + let coreDataOptions = CoreDataWrapper.Options( storageMechanism: options.storageMechanism, - entities: [ - SessionRecord.entityDescription, - SpanRecord.entityDescription, - MetadataRecord.entityDescription, - LogRecord.entityDescriptio - ] + entities: entities ) self.coreData = try CoreDataWrapper(options: coreDataOptions, logger: logger) } @@ -53,13 +54,19 @@ public class EmbraceStorage: Storage { extension EmbraceStorage { /// Deletes a record from the storage synchronously. /// - Parameter record: `NSManagedObject` to delete - public func delete(_ record: T) throws { + public func delete(_ record: T) { coreData.deleteRecord(record) } + /// Deletes records from the storage synchronously. + /// - Parameter record: `NSManagedObject` to delete + public func delete(_ records: [T]) { + coreData.deleteRecords(records) + } + /// Fetches all the records of the given type in the storage synchronously. /// - Returns: Array containing all the records of the given type - public func fetchAll() throws -> [T] { + public func fetchAll() -> [T] { let request = NSFetchRequest(entityName: T.entityName) return coreData.fetch(withRequest: request) } diff --git a/Sources/EmbraceStorageInternal/EmbraceStorageError.swift b/Sources/EmbraceStorageInternal/EmbraceStorageError.swift index bc8c4ce0..31fecf2d 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorageError.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorageError.swift @@ -3,7 +3,6 @@ // import Foundation -import GRDB public enum EmbraceStorageError: Error, Equatable { case cannotUpsertSpan(spanName: String, message: String) diff --git a/Sources/EmbraceStorageInternal/Migration/Migration.swift b/Sources/EmbraceStorageInternal/Migration/Migration.swift deleted file mode 100644 index 011276a0..00000000 --- a/Sources/EmbraceStorageInternal/Migration/Migration.swift +++ /dev/null @@ -1,27 +0,0 @@ -//// -//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -//// -// -// import Foundation -// import GRDB -// -// public protocol Migration { -// /// The identifier to register this migration under. Must be unique -// static var identifier: StringLiteralType { get } -// -// /// Controls how this migration handles foreign key constraints. Defaults to `immediate`. -// static var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { get } -// -// /// Operation that performs migration. -// /// See [GRDB Reference](https://swiftpackageindex.com/groue/grdb.swift/master/documentation/grdb/migrations). -// func perform(_ db: Database) throws -// } -// -// extension Migration { -// var identifier: StringLiteralType { Self.identifier } -// var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { Self.foreignKeyChecks } -// } -// -// extension Migration { -// public static var foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks { .immediate } -// } diff --git a/Sources/EmbraceStorageInternal/Migration/MigrationService.swift b/Sources/EmbraceStorageInternal/Migration/MigrationService.swift deleted file mode 100644 index eacf23fa..00000000 --- a/Sources/EmbraceStorageInternal/Migration/MigrationService.swift +++ /dev/null @@ -1,44 +0,0 @@ -//// -//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -//// -// -// import GRDB -// import EmbraceCommonInternal -// -// public protocol MigrationServiceProtocol { -// func perform(_ dbQueue: DatabaseWriter, migrations: [Migration]) throws -// } -// -// final public class MigrationService: MigrationServiceProtocol { -// -// let logger: InternalLogger -// -// public init(logger: InternalLogger) { -// self.logger = logger -// } -// -// public func perform(_ dbQueue: DatabaseWriter, migrations: [Migration]) throws { -// guard migrations.count > 0 else { -// logger.debug("No migrations to perform") -// return -// } -// -// var migrator = DatabaseMigrator() -// migrations.forEach { migration in -// migrator.registerMigration(migration.identifier, -// foreignKeyChecks: migration.foreignKeyChecks, -// migrate: migration.perform(_:)) -// } -// -// try dbQueue.read { db in -// if try migrator.hasCompletedMigrations(db) { -// logger.debug("DB is up to date") -// return -// } else { -// logger.debug("Running up to \(migrations.count) migrations") -// } -// } -// -// try migrator.migrate(dbQueue) -// } -// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240509_00_AddSpanRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240509_00_AddSpanRecordMigration.swift deleted file mode 100644 index 2a0527ea..00000000 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240509_00_AddSpanRecordMigration.swift +++ /dev/null @@ -1,36 +0,0 @@ -//// -//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -//// -// -// import GRDB -// -// struct AddSpanRecordMigration: Migration { -// static let identifier = "CreateSpanRecordTable" // DEV: Must not change -// -// func perform(_ db: GRDB.Database) throws { -// try db.create(table: SpanRecord.databaseTableName, options: .ifNotExists) { t in -// t.column(SpanRecord.Schema.id.name, .text).notNull() -// t.column(SpanRecord.Schema.name.name, .text).notNull() -// t.column(SpanRecord.Schema.traceId.name, .text).notNull() -// t.primaryKey([SpanRecord.Schema.traceId.name, SpanRecord.Schema.id.name]) -// -// t.column(SpanRecord.Schema.type.name, .text).notNull() -// t.column(SpanRecord.Schema.startTime.name, .datetime).notNull() -// t.column(SpanRecord.Schema.endTime.name, .datetime) -// -// t.column(SpanRecord.Schema.data.name, .blob).notNull() -// } -// -// let preventClosedSpanModification = """ -// CREATE TRIGGER IF NOT EXISTS prevent_closed_span_modification -// BEFORE UPDATE ON \(SpanRecord.databaseTableName) -// WHEN OLD.end_time IS NOT NULL -// BEGIN -// SELECT RAISE(ABORT,'Attempted to modify an already closed span.'); -// END; -// """ -// -// try db.execute(sql: preventClosedSpanModification) -// } -// -// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_00_AddSessionRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_00_AddSessionRecordMigration.swift deleted file mode 100644 index dbb9e690..00000000 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_00_AddSessionRecordMigration.swift +++ /dev/null @@ -1,39 +0,0 @@ -//// -//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -//// -// -// import GRDB -// -// struct AddSessionRecordMigration: Migration { -// static var identifier = "CreateSessionRecordTable" // DEV: Must not change -// -// func perform(_ db: GRDB.Database) throws { -// try db.create(table: SessionRecord.databaseTableName, options: .ifNotExists) { t in -// -// t.primaryKey(SessionRecord.Schema.id.name, .text).notNull() -// -// t.column(SessionRecord.Schema.state.name, .text).notNull() -// t.column(SessionRecord.Schema.processId.name, .text).notNull() -// t.column(SessionRecord.Schema.traceId.name, .text).notNull() -// t.column(SessionRecord.Schema.spanId.name, .text).notNull() -// -// t.column(SessionRecord.Schema.startTime.name, .datetime).notNull() -// t.column(SessionRecord.Schema.endTime.name, .datetime) -// t.column(SessionRecord.Schema.lastHeartbeatTime.name, .datetime).notNull() -// -// t.column(SessionRecord.Schema.coldStart.name, .boolean) -// .notNull() -// .defaults(to: false) -// -// t.column(SessionRecord.Schema.cleanExit.name, .boolean) -// .notNull() -// .defaults(to: false) -// -// t.column(SessionRecord.Schema.appTerminated.name, .boolean) -// .notNull() -// .defaults(to: false) -// -// t.column(SessionRecord.Schema.crashReportId.name, .text) -// } -// } -// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_01_AddMetadataRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_01_AddMetadataRecordMigration.swift deleted file mode 100644 index 6a7b0e64..00000000 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_01_AddMetadataRecordMigration.swift +++ /dev/null @@ -1,29 +0,0 @@ -//// -//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -//// -// -// import GRDB -// -// struct AddMetadataRecordMigration: Migration { -// -// static var identifier = "CreateMetadataRecordTable" // DEV: Must not change -// -// func perform(_ db: Database) throws { -// try db.create(table: MetadataRecord.databaseTableName, options: .ifNotExists) { t in -// -// t.column(MetadataRecord.Schema.key.name, .text).notNull() -// t.column(MetadataRecord.Schema.value.name, .text).notNull() -// t.column(MetadataRecord.Schema.type.name, .text).notNull() -// t.column(MetadataRecord.Schema.lifespan.name, .text).notNull() -// t.column(MetadataRecord.Schema.lifespanId.name, .text).notNull() -// t.column(MetadataRecord.Schema.collectedAt.name, .datetime).notNull() -// -// t.primaryKey([ -// MetadataRecord.Schema.key.name, -// MetadataRecord.Schema.type.name, -// MetadataRecord.Schema.lifespan.name, -// MetadataRecord.Schema.lifespanId.name -// ]) -// } -// } -// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_02_AddLogRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_02_AddLogRecordMigration.swift deleted file mode 100644 index 95710fd5..00000000 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240510_02_AddLogRecordMigration.swift +++ /dev/null @@ -1,21 +0,0 @@ -//// -//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -//// -// -// import GRDB -// -// struct AddLogRecordMigration: Migration { -// -// static var identifier = "CreateLogRecordTable" // DEV: Must not change -// -// func perform(_ db: Database) throws { -// try db.create(table: LogRecord.databaseTableName, options: .ifNotExists) { t in -// t.primaryKey(LogRecord.Schema.identifier.name, .text).notNull() -// t.column(LogRecord.Schema.processIdentifier.name, .integer).notNull() -// t.column(LogRecord.Schema.severity.name, .integer).notNull() -// t.column(LogRecord.Schema.body.name, .text).notNull() -// t.column(LogRecord.Schema.timestamp.name, .datetime).notNull() -// t.column(LogRecord.Schema.attributes.name, .text).notNull() -// } -// } -// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigration.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigration.swift deleted file mode 100644 index 0c71885a..00000000 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigration.swift +++ /dev/null @@ -1,73 +0,0 @@ -//// -//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -//// -// -// import GRDB -// -// struct AddProcessIdentifierToSpanRecordMigration: Migration { -// static var identifier = "AddProcessIdentifierToSpanRecord" // DEV: Must not change -// -// private static var tempSpansTableName = "spans_temp" -// -// func perform(_ db: GRDB.Database) throws { -// -// // create copy of `spans` table in `spans_temp` -// // include new column 'process_identifier' -// try db.create(table: Self.tempSpansTableName) { t in -// -// t.column(SpanRecord.Schema.id.name, .text).notNull() -// t.column(SpanRecord.Schema.traceId.name, .text).notNull() -// t.primaryKey([SpanRecord.Schema.traceId.name, SpanRecord.Schema.id.name]) -// -// t.column(SpanRecord.Schema.name.name, .text).notNull() -// t.column(SpanRecord.Schema.type.name, .text).notNull() -// t.column(SpanRecord.Schema.startTime.name, .datetime).notNull() -// t.column(SpanRecord.Schema.endTime.name, .datetime) -// t.column(SpanRecord.Schema.data.name, .blob).notNull() -// -// // include new column into `spans_temp` table -// t.column(SpanRecord.Schema.processIdentifier.name, .text).notNull() -// } -// -// // copy all existing data into temp table -// // include default value for `process_identifier` -// try db.execute(literal: """ -// INSERT INTO 'spans_temp' ( -// 'id', -// 'trace_id', -// 'name', -// 'type', -// 'start_time', -// 'end_time', -// 'data', -// 'process_identifier' -// ) SELECT -// id, -// trace_id, -// name, -// type, -// start_time, -// end_time, -// data, -// 'c0ffee' -// FROM 'spans' -// """) -// -// // drop original table -// try db.drop(table: SpanRecord.databaseTableName) -// -// // rename temp table to be original table -// try db.rename(table: Self.tempSpansTableName, to: SpanRecord.databaseTableName) -// -// // Create Trigger on new spans table to prevent endTime from being modified on SpanRecord -// try db.execute(sql: -// """ -// CREATE TRIGGER IF NOT EXISTS prevent_closed_span_modification -// BEFORE UPDATE ON \(SpanRecord.databaseTableName) -// WHEN OLD.end_time IS NOT NULL -// BEGIN -// SELECT RAISE(ABORT,'Attempted to modify an already closed span.'); -// END; -// """ ) -// } -// } diff --git a/Sources/EmbraceStorageInternal/Migration/Migrations/Migrations+Current.swift b/Sources/EmbraceStorageInternal/Migration/Migrations/Migrations+Current.swift deleted file mode 100644 index 354b136b..00000000 --- a/Sources/EmbraceStorageInternal/Migration/Migrations/Migrations+Current.swift +++ /dev/null @@ -1,20 +0,0 @@ -//// -//// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -//// -// -// import Foundation -// -// extension Array where Element == Migration { -// -// public static var current: [Migration] { -// return [ -// // register migrations here -// // order matters -// AddSpanRecordMigration(), -// AddSessionRecordMigration(), -// AddMetadataRecordMigration(), -// AddLogRecordMigration(), -// AddProcessIdentifierToSpanRecordMigration() -// ] -// } -// } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift new file mode 100644 index 00000000..0e645552 --- /dev/null +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift @@ -0,0 +1,63 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import OpenTelemetryApi + +public protocol LogRepository { + func create( + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date, + attributes: [String: AttributeValue] + ) -> LogRecord + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [LogRecord] + func remove(logs: [LogRecord]) + func removeAllLogs() +} + +extension EmbraceStorage { + + public func create( + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date, + attributes: [String : OpenTelemetryApi.AttributeValue] + ) -> LogRecord { + return LogRecord.create( + context: coreData.context, + id: id, + processId: processId, + severity: severity, + body: body, + timestamp: timestamp, + attributes: attributes + ) + } + + public func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [LogRecord] { + let request = LogRecord.createFetchRequest() + request.predicate = NSPredicate(format: "processIdRaw != %@", processIdentifier.hex) + + return coreData.fetch(withRequest: request) + } + + public func removeAllLogs() { + remove(logs: getAll()) + } + + public func remove(logs: [LogRecord]) { + coreData.deleteRecords(logs) + } + + public func getAll() -> [LogRecord] { + let request = LogRecord.createFetchRequest() + return coreData.fetch(withRequest: request) + } +} diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift index 4dd59a79..2aa873a0 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift @@ -3,7 +3,6 @@ // import Foundation -import GRDB import EmbraceCommonInternal public protocol EmbraceStorageMetadataFetcher: AnyObject { @@ -191,7 +190,7 @@ extension EmbraceStorage { } /// Returns the permanent required resource for the given key. - public func fetchRequiredPermanentResource(key: String) throws -> MetadataRecord? { + public func fetchRequiredPermanentResource(key: String) -> MetadataRecord? { return fetchMetadata(key: key, type: .requiredResource, lifespan: .permanent) } @@ -208,7 +207,7 @@ extension EmbraceStorage { } /// Returns all records with types `.requiredResource` or `.resource` that are tied to a given session id - public func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + public func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { guard let session = fetchSession(id: sessionId) else { return [] @@ -227,7 +226,7 @@ extension EmbraceStorage { } /// Returns all records with types `.requiredResource` or `.resource` that are tied to a given process id - public func fetchResourcesForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] { + public func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( @@ -242,7 +241,7 @@ extension EmbraceStorage { } /// Returns all records of the `.customProperty` type that are tied to a given session id - public func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + public func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { guard let session = fetchSession(id: sessionId) else { return [] } @@ -260,7 +259,7 @@ extension EmbraceStorage { } /// Returns all records of the `.personaTag` type that are tied to a given session id - public func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + public func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { guard let session = fetchSession(id: sessionId) else { return [] } @@ -278,7 +277,7 @@ extension EmbraceStorage { } /// Returns all records of the `.personaTag` type that are tied to a given process id - public func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] { + public func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift index 7f5f1c8f..82d348b3 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift @@ -4,7 +4,6 @@ import Foundation import EmbraceCommonInternal -import GRDB extension EmbraceStorage { /// Adds a session to the storage synchronously. diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index cb67c423..05950856 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -6,7 +6,6 @@ import Foundation import EmbraceCommonInternal import EmbraceSemantics import CoreData -import GRDB extension EmbraceStorage { diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift index bc98d79b..1e8c3c55 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift @@ -7,5 +7,4 @@ import CoreData public protocol EmbraceStorageRecord: NSManagedObject { static var entityName: String { get } - static var entityDescription: NSEntityDescription { get } } diff --git a/Sources/EmbraceStorageInternal/Records/Log/EmbraceStorage+Log.swift b/Sources/EmbraceStorageInternal/Records/Log/EmbraceStorage+Log.swift deleted file mode 100644 index b5848a49..00000000 --- a/Sources/EmbraceStorageInternal/Records/Log/EmbraceStorage+Log.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import GRDB -import EmbraceCommonInternal - -extension LogRecord: Identifiable { - public var id: String { - identifier.value.uuidString - } -} - -extension EmbraceStorage { - func writeLog(_ log: LogRecord) throws { - try dbQueue.write { db in - try log.insert(db) - } - } - - public func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) throws -> [LogRecord] { - return try dbQueue.read { db in - let query = LogRecord.filter(LogRecord.Schema.processIdentifier != processIdentifier.value) - return try LogRecord.fetchAll(db, query) - } - } - - public func removeAllLogs() throws { - try dbQueue.write { db in - _ = try LogRecord.deleteAll(db) - } - } - - public func remove(logs: [LogRecord]) throws { - try dbQueue.write { db in - let logIds = logs.map { $0.id } - _ = try LogRecord.filter( - logIds.contains(LogRecord.Schema.identifier) - ).deleteAll(db) - } - } - - public func getAll() throws -> [LogRecord] { - try dbQueue.read { db in - try LogRecord.fetchAll(db) - } - } - - public func create(_ log: LogRecord, completion: (Result) -> Void) { - do { - try writeLog(log) - completion(.success(log)) - } catch let exception { - completion(.failure(exception)) - } - } -} diff --git a/Sources/EmbraceStorageInternal/Records/Log/LogRecord.swift b/Sources/EmbraceStorageInternal/Records/Log/LogRecord.swift deleted file mode 100644 index 3d3269f8..00000000 --- a/Sources/EmbraceStorageInternal/Records/Log/LogRecord.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceCommonInternal -import GRDB - -public struct LogRecord { - public var identifier: LogIdentifier - public var processIdentifier: ProcessIdentifier - public var severity: LogSeverity - public var body: String - public var timestamp: Date - public var attributes: [String: PersistableValue] - - public init(identifier: LogIdentifier, - processIdentifier: ProcessIdentifier, - severity: LogSeverity, - body: String, - attributes: [String: PersistableValue], - timestamp: Date = Date()) { - self.identifier = identifier - self.processIdentifier = processIdentifier - self.severity = severity - self.body = body - self.timestamp = timestamp - self.attributes = attributes - } -} - -extension LogRecord: FetchableRecord, PersistableRecord { - public static let databaseTableName: String = "logs" - - public static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase - public static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase - public static let persistenceConflictPolicy = PersistenceConflictPolicy(insert: .replace, update: .replace) - - public func encode(to container: inout PersistenceContainer) { - container[Schema.identifier] = identifier.value.uuidString - container[Schema.processIdentifier] = processIdentifier.value - container[Schema.severity] = severity.rawValue - container[Schema.body] = body - container[Schema.timestamp] = timestamp - if let encodedAttributes = try? JSONEncoder().encode(attributes), - let attributesJsonString = String(data: encodedAttributes, encoding: .utf8) { - container[Schema.attributes] = attributesJsonString - } else { - container[Schema.attributes] = "" - } - } - - public init(row: Row) { - identifier = LogIdentifier(value: row[Schema.identifier]) - processIdentifier = ProcessIdentifier(value: row[Schema.processIdentifier]) - severity = LogSeverity(rawValue: row[Schema.severity]) ?? .info - body = row[Schema.body] - timestamp = row[Schema.timestamp] - if let jsonString = row[Schema.attributes] as? String, - let data = jsonString.data(using: .utf8), - let json = try? JSONDecoder().decode([String: PersistableValue].self, from: data) { - attributes = json - } else { - attributes = [:] - } - } -} - -extension LogRecord: Codable { - enum CodingKeys: String, CodingKey { - case identifier, processIdentifier, severity, body, timestamp, attributes - } -} - -extension LogRecord { - struct Schema { - static var identifier: Column { Column("identifier") } - static var processIdentifier: Column { Column("process_identifier") } - static var severity: Column { Column("severity") } - static var body: Column { Column("body") } - static var timestamp: Column { Column("timestamp") } - static var attributes: Column { Column("attributes") } - } -} diff --git a/Sources/EmbraceStorageInternal/Records/Log/LogRecordRepository.swift b/Sources/EmbraceStorageInternal/Records/Log/LogRecordRepository.swift deleted file mode 100644 index e67ba43a..00000000 --- a/Sources/EmbraceStorageInternal/Records/Log/LogRecordRepository.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceCommonInternal - -public protocol LogRepository { - func create(_ log: LogRecord, completion: (Result) -> Void) - func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) throws -> [LogRecord] - func remove(logs: [LogRecord]) throws - func removeAllLogs() throws -} diff --git a/Sources/EmbraceStorageInternal/Records/Log/PersistableValue.swift b/Sources/EmbraceStorageInternal/Records/Log/PersistableValue.swift deleted file mode 100644 index 92a42657..00000000 --- a/Sources/EmbraceStorageInternal/Records/Log/PersistableValue.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation - -public enum PersistableValue: Equatable, CustomStringConvertible, Hashable, Codable { - case string(String) - case bool(Bool) - case int(Int) - case double(Double) - case stringArray([String]) - case boolArray([Bool]) - case intArray([Int]) - case doubleArray([Double]) - - public var description: String { - switch self { - case let .string(value): - return value - case let .bool(value): - return value ? "true" : "false" - case let .int(value): - return String(value) - case let .double(value): - return String(value) - case let .stringArray(value): - return value.description - case let .boolArray(value): - return value.description - case let .intArray(value): - return value.description - case let .doubleArray(value): - return value.description - } - } - - public init?(_ value: Any) { - switch value { - case let val as String: - self = .string(val) - case let val as Bool: - self = .bool(val) - case let val as Int: - self = .int(val) - case let val as Double: - self = .double(val) - case let val as [String]: - self = .stringArray(val) - case let val as [Bool]: - self = .boolArray(val) - case let val as [Int]: - self = .intArray(val) - case let val as [Double]: - self = .doubleArray(val) - default: - return nil - } - } -} - -public extension PersistableValue { - init(_ value: String) { - self = .string(value) - } - - init(_ value: Bool) { - self = .bool(value) - } - - init(_ value: Int) { - self = .int(value) - } - - init(_ value: Double) { - self = .double(value) - } - - init(_ value: [String]) { - self = .stringArray(value) - } - - init(_ value: [Int]) { - self = .intArray(value) - } - - init(_ value: [Double]) { - self = .doubleArray(value) - } -} diff --git a/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift new file mode 100644 index 00000000..7ae4e13c --- /dev/null +++ b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift @@ -0,0 +1,63 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import CoreData +import OpenTelemetryApi + +public enum LogAttributeType: Int { + case string, int, double, bool +} + +public class LogAttributeRecord: NSManagedObject { + @NSManaged public var key: String + @NSManaged public var valueRaw: String + @NSManaged public var typeRaw: Int // LogAttributeType + @NSManaged public var log: LogRecord + + public var value: AttributeValue { + get { + let type = LogAttributeType(rawValue: typeRaw) ?? .string + + switch type { + case .int: return AttributeValue(Int(valueRaw) ?? 0) + case .double: return AttributeValue(Double(valueRaw) ?? 0) + case .bool: return AttributeValue(Bool(valueRaw) ?? false) + default: return AttributeValue(valueRaw) + } + } + + set { + valueRaw = newValue.description + typeRaw = Self.typeForValue(newValue).rawValue + } + } + + public static func create( + context: NSManagedObjectContext, + key: String, + value: AttributeValue, + log: LogRecord + ) -> LogAttributeRecord { + let record = LogAttributeRecord(context: context) + record.key = key + record.value = value + record.log = log + + return record + } + + private static func typeForValue(_ value: AttributeValue) -> LogAttributeType { + switch value { + case .int(_): return .int + case .double(_): return .double + case .bool(_): return .bool + default: return .string + } + } +} + +extension LogAttributeRecord: EmbraceStorageRecord { + public static var entityName = "LogAttributeRecord" +} diff --git a/Sources/EmbraceStorageInternal/Records/LogRecord.swift b/Sources/EmbraceStorageInternal/Records/LogRecord.swift new file mode 100644 index 00000000..6220b35d --- /dev/null +++ b/Sources/EmbraceStorageInternal/Records/LogRecord.swift @@ -0,0 +1,159 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import OpenTelemetryApi +import CoreData + +public class LogRecord: NSManagedObject { + @NSManaged public var idRaw: String // LogIdentifier + @NSManaged public var processIdRaw: String // ProcessIdentifier + @NSManaged public var severityRaw: Int // LogSeverity + @NSManaged public var body: String + @NSManaged public var timestamp: Date + @NSManaged public var attributes: [LogAttributeRecord] + + public var processId: ProcessIdentifier? { + return ProcessIdentifier(hex: processIdRaw) + } + + public var severity: LogSeverity { + return LogSeverity(rawValue: severityRaw) ?? .info + } + + static func create( + context: NSManagedObjectContext, + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date = Date(), + attributes: [String: AttributeValue] + ) -> LogRecord { + let record = LogRecord(context: context) + record.idRaw = id.toString + record.processIdRaw = processId.hex + record.severityRaw = severity.rawValue + record.body = body + record.timestamp = timestamp + + for (key, value) in attributes { + let attribute = LogAttributeRecord.create( + context: context, + key: key, + value: value, + log: record + ) + record.attributes.append(attribute) + } + + return record + } + + static func createFetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: entityName) + } +} + +extension LogRecord { + public func attribute(forKey key: String) -> LogAttributeRecord? { + return attributes.first(where: { $0.key == key }) + } + + public func setAttributeValue(value: AttributeValue, forKey key: String) { + if let attribute = attribute(forKey: key) { + attribute.value = value + } + + guard let context = managedObjectContext else { + return + } + + let attribute = LogAttributeRecord.create(context: context, key: key, value: value, log: self) + attributes.append(attribute) + } +} + +extension LogRecord: EmbraceStorageRecord { + public static var entityName = "LogRecord" + + static public var entityDescriptions: [NSEntityDescription] { + let entity = NSEntityDescription() + entity.name = entityName + entity.managedObjectClassName = NSStringFromClass(LogRecord.self) + + let child = NSEntityDescription() + child.name = LogAttributeRecord.entityName + child.managedObjectClassName = NSStringFromClass(LogAttributeRecord.self) + + // parent + let idAttribute = NSAttributeDescription() + idAttribute.name = "idRaw" + idAttribute.attributeType = .stringAttributeType + + let processIdAttribute = NSAttributeDescription() + processIdAttribute.name = "processIdRaw" + processIdAttribute.attributeType = .stringAttributeType + + let severityAttribute = NSAttributeDescription() + severityAttribute.name = "severityRaw" + severityAttribute.attributeType = .integer64AttributeType + + let bodyAttribute = NSAttributeDescription() + bodyAttribute.name = "body" + bodyAttribute.attributeType = .stringAttributeType + + let timestampAttribute = NSAttributeDescription() + timestampAttribute.name = "timestamp" + timestampAttribute.attributeType = .dateAttributeType + + // child + let keyAttribute = NSAttributeDescription() + keyAttribute.name = "key" + keyAttribute.attributeType = .stringAttributeType + + let valueAttribute = NSAttributeDescription() + valueAttribute.name = "valueRaw" + valueAttribute.attributeType = .stringAttributeType + + let typeAttribute = NSAttributeDescription() + typeAttribute.name = "typeRaw" + typeAttribute.attributeType = .integer64AttributeType + + // relationships + let parentRelationship = NSRelationshipDescription() + let childRelationship = NSRelationshipDescription() + + parentRelationship.name = "attributes" + parentRelationship.deleteRule = .cascadeDeleteRule + parentRelationship.destinationEntity = child + parentRelationship.inverseRelationship = childRelationship + + childRelationship.name = "log" + childRelationship.minCount = 1 + childRelationship.maxCount = 1 + childRelationship.destinationEntity = entity + childRelationship.inverseRelationship = parentRelationship + + // set properties + entity.properties = [ + idAttribute, + processIdAttribute, + severityAttribute, + bodyAttribute, + timestampAttribute, + parentRelationship + ] + + child.properties = [ + keyAttribute, + valueAttribute, + typeAttribute, + childRelationship + ] + + return [entity, child] + } +} diff --git a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadType+DatabaseValueConvertible.swift b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadType+DatabaseValueConvertible.swift deleted file mode 100644 index 88ec338e..00000000 --- a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadType+DatabaseValueConvertible.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import GRDB - -extension EmbraceUploadType: DatabaseValueConvertible { - var databaseValue: DatabaseValue { - return self.rawValue.databaseValue - } - - static func fromDatabaseValue(_ dbValue: DatabaseValue) -> EmbraceUploadType? { - guard let rawValue = Int.fromDatabaseValue(dbValue) else { - return nil - } - return EmbraceUploadType(rawValue: rawValue) - } -} diff --git a/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift b/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift index bec417f2..c3e2ab47 100644 --- a/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift +++ b/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift @@ -3,7 +3,6 @@ // import Foundation -import GRDB import CoreData /// Represents a cached upload data in the storage diff --git a/bin/build_dependencies.sh b/bin/build_dependencies.sh index cec30474..f67b0863 100755 --- a/bin/build_dependencies.sh +++ b/bin/build_dependencies.sh @@ -1,7 +1,7 @@ #!/bin/bash set -x -DEPENDENCIES=("KSCrash" "GRDB.swift" "opentelemetry-swift") +DEPENDENCIES=("KSCrash" "opentelemetry-swift") SCRIPT_DIR=$(dirname $0) DEPENDENCIES_DIR="${SCRIPT_DIR}/dependencies" TEMP_DIR="${DEPENDENCIES_DIR}/temp" diff --git a/bin/dependencies/build_grdb.swift.sh b/bin/dependencies/build_grdb.swift.sh deleted file mode 100644 index 48e564b0..00000000 --- a/bin/dependencies/build_grdb.swift.sh +++ /dev/null @@ -1,7 +0,0 @@ -REPO_DIR=$1 -BUILD_DIR=$2 - -make --directory "$REPO_DIR" test_universal_xcframework - -mv -v "$(PWD)/Tests/products/GRDB.xcframework" "$BUILD_DIR" -rm -rf "$(PWD)/Tests/products" diff --git a/bin/templates/EmbraceIO.podspec.tpl b/bin/templates/EmbraceIO.podspec.tpl index 9ade575f..5e02be46 100644 --- a/bin/templates/EmbraceIO.podspec.tpl +++ b/bin/templates/EmbraceIO.podspec.tpl @@ -75,14 +75,12 @@ Pod::Spec.new do |spec| storage.vendored_frameworks = "xcframeworks/EmbraceStorageInternal.xcframework" storage.dependency "EmbraceIO/EmbraceCommonInternal" storage.dependency "EmbraceIO/EmbraceSemantics" - storage.dependency "EmbraceIO/GRDB" end spec.subspec 'EmbraceUploadInternal' do |upload| upload.vendored_frameworks = "xcframeworks/EmbraceUploadInternal.xcframework" upload.dependency "EmbraceIO/EmbraceCommonInternal" upload.dependency "EmbraceIO/EmbraceOTelInternal" - upload.dependency "EmbraceIO/GRDB" end spec.subspec 'EmbraceCrashlyticsSupport' do |cs| @@ -110,10 +108,6 @@ Pod::Spec.new do |spec| otelSdk.dependency "EmbraceIO/OpenTelemetryApi" end - spec.subspec 'GRDB' do |grdb| - grdb.vendored_frameworks = "xcframeworks/GRDB.xcframework" - end - spec.subspec 'KSCrash' do |kscrash| kscrash.dependency "EmbraceIO/KSCrashCore" kscrash.dependency "EmbraceIO/KSCrashRecording" From f459371330b7cbb17ec871865382c24fe461e46c Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Wed, 29 Jan 2025 14:21:46 -0300 Subject: [PATCH 10/17] WIP --- .../Capture/ResourceCaptureService.swift | 18 ++-- .../DeviceIdentifier+Persistence.swift | 32 +++---- .../Logs/EmbraceLogAttributesBuilder.swift | 21 ++--- .../Internal/Logs/LogController.swift | 24 +++-- .../ResourceStorageExporter.swift | 4 +- .../Payload/Builders/LogPayloadBuilder.swift | 22 ++--- .../Builders/SessionPayloadBuilder.swift | 38 +++----- .../Builders/SpansPayloadBuilder.swift | 8 +- .../Payload/Utils/PayloadUtils.swift | 16 +--- .../Metadata/MetadataHandler+Personas.swift | 12 +-- .../Public/Metadata/MetadataHandler.swift | 89 +++++++++---------- .../DataRecovery/UnsentDataHandler.swift | 72 +++++---------- .../CoreDataWrapper.swift | 18 ++++ .../EmbraceStorage.swift | 2 +- .../Records/EmbraceStorage+Log.swift | 2 +- .../Records/EmbraceStorage+Metadata.swift | 30 +++---- .../Records/LogAttributeRecord.swift | 6 +- 17 files changed, 171 insertions(+), 243 deletions(-) diff --git a/Sources/EmbraceCore/Capture/ResourceCaptureService.swift b/Sources/EmbraceCore/Capture/ResourceCaptureService.swift index 0fd34720..eefaf115 100644 --- a/Sources/EmbraceCore/Capture/ResourceCaptureService.swift +++ b/Sources/EmbraceCore/Capture/ResourceCaptureService.swift @@ -21,16 +21,12 @@ class ResourceCaptureService: CaptureService { extension EmbraceStorage: ResourceCaptureServiceHandler { func addResource(key: String, value: AttributeValue) { - do { - _ = try addMetadata( - key: key, - value: value.description, - type: .requiredResource, - lifespan: .process, - lifespanId: ProcessIdentifier.current.hex - ) - } catch { - Embrace.logger.error("Failed to capture resource: \(error.localizedDescription)") - } + _ = addMetadata( + key: key, + value: value.description, + type: .requiredResource, + lifespan: .process, + lifespanId: ProcessIdentifier.current.hex + ) } } diff --git a/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift b/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift index cd96ce69..1fa3b114 100644 --- a/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift +++ b/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift @@ -12,16 +12,12 @@ extension DeviceIdentifier { static func retrieve(from storage: EmbraceStorage?) -> DeviceIdentifier { // retrieve from storage if let storage = storage { - do { - if let resource = try storage.fetchRequiredPermanentResource(key: resourceKey) { - if let uuid = UUID(withoutHyphen: resource.value) { - return DeviceIdentifier(value: uuid) - } - - Embrace.logger.warning("Failed to convert device.id back into a UUID. Possibly corrupted!") + if let resource = storage.fetchRequiredPermanentResource(key: resourceKey) { + if let uuid = UUID(withoutHyphen: resource.value) { + return DeviceIdentifier(value: uuid) } - } catch let e { - Embrace.logger.error("Failed to fetch device id from database \(e.localizedDescription)") + + Embrace.logger.warning("Failed to convert device.id back into a UUID. Possibly corrupted!") } } @@ -29,18 +25,12 @@ extension DeviceIdentifier { let uuid = KeychainAccess.deviceId let deviceId = DeviceIdentifier(value: uuid) - if let storage = storage { - do { - try storage.addMetadata( - key: resourceKey, - value: deviceId.hex, - type: .requiredResource, - lifespan: .permanent - ) - } catch let e { - Embrace.logger.error("Failed to add device id to database \(e.localizedDescription)") - } - } + storage?.addMetadata( + key: resourceKey, + value: deviceId.hex, + type: .requiredResource, + lifespan: .permanent + ) return deviceId } diff --git a/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift b/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift index 52bd6537..df312f5a 100644 --- a/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift +++ b/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift @@ -69,18 +69,19 @@ class EmbraceLogAttributesBuilder { let storage = storage else { return self } - if let customProperties = try? storage.fetchCustomPropertiesForSessionId(sessionId) { - customProperties.forEach { record in - guard UserResourceKey(rawValue: record.key) == nil else { - // prevent UserResource keys from appearing in properties - // will be sent in MetadataPayload instead - return - } - - let key = String(format: LogSemantics.keyPropertiesPrefix, record.key) - attributes[key] = record.value + + let customProperties = storage.fetchCustomPropertiesForSessionId(sessionId) + customProperties.forEach { record in + guard UserResourceKey(rawValue: record.key) == nil else { + // prevent UserResource keys from appearing in properties + // will be sent in MetadataPayload instead + return } + + let key = String(format: LogSemantics.keyPropertiesPrefix, record.key) + attributes[key] = record.value } + return self } diff --git a/Sources/EmbraceCore/Internal/Logs/LogController.swift b/Sources/EmbraceCore/Internal/Logs/LogController.swift index 43872823..1558da27 100644 --- a/Sources/EmbraceCore/Internal/Logs/LogController.swift +++ b/Sources/EmbraceCore/Internal/Logs/LogController.swift @@ -54,14 +54,10 @@ class LogController: LogControllable { guard let storage = storage else { return } - do { - let logs: [LogRecord] = try storage.fetchAll(excludingProcessIdentifier: .current) - if logs.count > 0 { - send(batches: divideInBatches(logs)) - } - } catch let exception { - Error.couldntAccessBatches(reason: exception.localizedDescription).log() - try? storage.removeAllLogs() + + let logs: [LogRecord] = storage.fetchAll(excludingProcessIdentifier: .current) + if logs.count > 0 { + send(batches: divideInBatches(logs)) } } @@ -236,7 +232,7 @@ private extension LogController { return } - try? self.storage?.remove(logs: logs) + self.storage?.remove(logs: logs) } } catch let exception { Error.couldntCreatePayload(reason: exception.localizedDescription).log() @@ -277,9 +273,9 @@ private extension LogController { var resources: [MetadataRecord] = [] if let sessionId = sessionId { - resources = try storage.fetchResourcesForSessionId(sessionId) + resources = storage.fetchResourcesForSessionId(sessionId) } else { - resources = try storage.fetchResourcesForProcessId(processId) + resources = storage.fetchResourcesForProcessId(processId) } return ResourcePayload(from: resources) @@ -295,12 +291,12 @@ private extension LogController { var metadata: [MetadataRecord] = [] if let sessionId = sessionId { - let properties = try storage.fetchCustomPropertiesForSessionId(sessionId) - let tags = try storage.fetchPersonaTagsForSessionId(sessionId) + let properties = storage.fetchCustomPropertiesForSessionId(sessionId) + let tags = storage.fetchPersonaTagsForSessionId(sessionId) metadata.append(contentsOf: properties) metadata.append(contentsOf: tags) } else { - metadata = try storage.fetchPersonaTagsForProcessId(processId) + metadata = storage.fetchPersonaTagsForProcessId(processId) } return MetadataPayload(from: metadata) diff --git a/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift b/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift index 8dbbdaf6..d3c56621 100644 --- a/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift +++ b/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift @@ -30,9 +30,7 @@ class ResourceStorageExporter: EmbraceResourceProvider { return Resource() } - guard let records = try? storage.fetchAllResources() else { - return Resource() - } + let records = storage.fetchAllResources() var attributes: [String: AttributeValue] = records.reduce(into: [:]) { partialResult, record in partialResult[record.key] = .string(record.value) diff --git a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift index 18ba568b..9edc8ac3 100644 --- a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift @@ -36,20 +36,16 @@ struct LogPayloadBuilder { var metadata: [MetadataRecord] = [] if let storage = storage { - do { - if let sessionId = sessionId { - resources = try storage.fetchResourcesForSessionId(sessionId) + if let sessionId = sessionId { + resources = storage.fetchResourcesForSessionId(sessionId) - let properties = try storage.fetchCustomPropertiesForSessionId(sessionId) - let tags = try storage.fetchPersonaTagsForSessionId(sessionId) - metadata.append(contentsOf: properties) - metadata.append(contentsOf: tags) - } else { - resources = try storage.fetchResourcesForProcessId(ProcessIdentifier.current) - metadata = try storage.fetchPersonaTagsForProcessId(ProcessIdentifier.current) - } - } catch { - Embrace.logger.error("Error fetching resources for crash log.") + let properties = storage.fetchCustomPropertiesForSessionId(sessionId) + let tags = storage.fetchPersonaTagsForSessionId(sessionId) + metadata.append(contentsOf: properties) + metadata.append(contentsOf: tags) + } else { + resources = storage.fetchResourcesForProcessId(ProcessIdentifier.current) + metadata = storage.fetchPersonaTagsForProcessId(ProcessIdentifier.current) } } diff --git a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift index bb80a663..76c20ba1 100644 --- a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift @@ -15,34 +15,22 @@ class SessionPayloadBuilder { return nil } - var resource: MetadataRecord? - - do { - // fetch resource - resource = try storage.fetchRequiredPermanentResource(key: resourceName) - } catch { - Embrace.logger.debug("Error fetching \(resourceName) resource!") - } - // increment counter or create resource if needed + var resource = storage.fetchRequiredPermanentResource(key: resourceName) var counter: Int = -1 - do { - if var resource = resource { - counter = (Int(resource.value) ?? 0) + 1 - resource.value = String(counter) - storage.save() - } else { - resource = try storage.addMetadata( - key: resourceName, - value: "1", - type: .requiredResource, - lifespan: .permanent - ) - counter = 1 - } - } catch { - Embrace.logger.debug("Error updating \(resourceName) resource!") + if let resource = resource { + counter = (Int(resource.value) ?? 0) + 1 + resource.value = String(counter) + storage.save() + } else { + resource = storage.addMetadata( + key: resourceName, + value: "1", + type: .requiredResource, + lifespan: .permanent + ) + counter = 1 } // build spans diff --git a/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift index acd32ac1..8c5c8f7a 100644 --- a/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift @@ -19,16 +19,10 @@ class SpansPayloadBuilder { ) -> (spans: [SpanPayload], spanSnapshots: [SpanPayload]) { let endTime = sessionRecord.endTime ?? sessionRecord.lastHeartbeatTime - var records: [SpanRecord] = [] // fetch spans that started during the session // ignore spans where emb.type == session - do { - records = try storage.fetchSpans(for: sessionRecord, ignoreSessionSpans: true, limit: spanCountLimit) - } catch { - Embrace.logger.error("Error fetching spans for session \(sessionRecord.id):\n\(error.localizedDescription)") - return ([], []) - } + let records = storage.fetchSpans(for: sessionRecord, ignoreSessionSpans: true, limit: spanCountLimit) // decode spans and separate them by closed/open var spans: [SpanPayload] = [] diff --git a/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift b/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift index 2289a75e..823ead8a 100644 --- a/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift +++ b/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift @@ -16,13 +16,7 @@ class PayloadUtils { return [] } - do { - return try fetcher.fetchResourcesForSessionId(sessionId) - } catch let e { - Embrace.logger.error("Failed to fetch resource records from storage: \(e.localizedDescription)") - } - - return [] + return fetcher.fetchResourcesForSessionId(sessionId) } static func fetchCustomProperties( @@ -34,13 +28,7 @@ class PayloadUtils { return [] } - do { - return try fetcher.fetchCustomPropertiesForSessionId(sessionId) - } catch let e { - Embrace.logger.error("Failed to fetch custom properties from storage: \(e.localizedDescription)") - } - - return [] + return fetcher.fetchCustomPropertiesForSessionId(sessionId) } static func convertSpanAttributes(_ attributes: [String: AttributeValue]) -> [Attribute] { diff --git a/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift b/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift index d88c0e31..d283aabc 100644 --- a/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift +++ b/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift @@ -15,14 +15,10 @@ extension MetadataHandler { } var records: [MetadataRecord] = [] - do { - if let sessionId = sessionController?.currentSession?.id { - records = try storage.fetchPersonaTagsForSessionId(sessionId) - } else { - records = try storage.fetchPersonaTagsForProcessId(ProcessIdentifier.current) - } - } catch { - Embrace.logger.error("Error fetching persona tags!\n\(error.localizedDescription)") + if let sessionId = sessionController?.currentSession?.id { + records = storage.fetchPersonaTagsForSessionId(sessionId) + } else { + records = storage.fetchPersonaTagsForProcessId(ProcessIdentifier.current) } return records.map { PersonaTag($0.key) } diff --git a/Sources/EmbraceCore/Public/Metadata/MetadataHandler.swift b/Sources/EmbraceCore/Public/Metadata/MetadataHandler.swift index 49faa116..e51daa7c 100644 --- a/Sources/EmbraceCore/Public/Metadata/MetadataHandler.swift +++ b/Sources/EmbraceCore/Public/Metadata/MetadataHandler.swift @@ -35,23 +35,25 @@ public class MetadataHandler: NSObject { self.sessionController = sessionController // tmp core data stack + // only created if the db file is found + // the entire data gets migrated to the real db and the file is removed + // that means this should only be executed once let coreDataStackName = "EmbraceMetadataTmp" - var storageMechanism: StorageMechanism = .inMemory(name: coreDataStackName) // in memory only used for tests - - if let storage = storage, - let url = storage.options.storageMechanism.baseUrl { - storageMechanism = .onDisk(name: coreDataStackName, baseURL: url) - } - - let options = CoreDataWrapper.Options( - storageMechanism: storageMechanism, - entities: [MetadataRecordTmp.entityDescription] - ) - - do { - self.coreData = try CoreDataWrapper(options: options, logger: Embrace.logger) - } catch { - Embrace.logger.error("Error setting up temp metadata database!:\n\(error.localizedDescription)") + if let url = storage?.options.storageMechanism.baseUrl, + FileManager.default.fileExists(atPath: url.appendingPathComponent(coreDataStackName + ".sqlite").path) { + + let options = CoreDataWrapper.Options( + storageMechanism: .onDisk(name: coreDataStackName, baseURL: url), + entities: [MetadataRecordTmp.entityDescription] + ) + + do { + self.coreData = try CoreDataWrapper(options: options, logger: Embrace.logger) + } catch { + Embrace.logger.error("Error setting up temp metadata database!:\n\(error.localizedDescription)") + self.coreData = nil + } + } else { self.coreData = nil } @@ -98,7 +100,7 @@ public class MetadataHandler: NSObject { let lifespanContext = try currentContext(for: lifespan.recordLifespan) - let record = try storage.addMetadata( + let record = storage.addMetadata( key: key, value: validateValue(value), type: type, @@ -136,7 +138,7 @@ public class MetadataHandler: NSObject { type: MetadataRecordType, lifespan: MetadataLifespan = .session ) throws { - try storage?.updateMetadata( + storage?.updateMetadata( key: key, value: validateValue(value), type: type, @@ -169,7 +171,7 @@ public class MetadataHandler: NSObject { /// /// - Throws: `MetadataError.invalidSession` if a metadata with a `.session` lifespan is removed when there's no active session. func remove(key: String, type: MetadataRecordType, lifespan: MetadataLifespan = .session) throws { - try storage?.removeMetadata( + storage?.removeMetadata( key: key, type: type, lifespan: lifespan.recordLifespan, @@ -192,7 +194,7 @@ public class MetadataHandler: NSObject { } func removeAll(type: MetadataRecordType, lifespans: [MetadataLifespan]) throws { - try storage?.removeAllMetadata( + storage?.removeAllMetadata( type: type, lifespans: lifespans.map { $0.recordLifespan } ) @@ -239,34 +241,23 @@ extension MetadataLifespan { // tmp core data stack extension MetadataHandler { func cloneDataBase() { -// guard let coreData = coreData, -// let storage = storage else { -// return -// } -// -// let request = NSFetchRequest(entityName: MetadataRecordTmp.entityName) -// let oldRecords = coreData.fetch(withRequest: request) -// coreData.deleteRecords(oldRecords) -// -// do { -// var newRecords: [MetadataRecord] = [] -// try storage.dbQueue.read { db in -// newRecords = try MetadataRecord.fetchAll(db) -// } -// -// coreData.context.perform { -// for record in newRecords { -// _ = MetadataRecordTmp.create(context: coreData.context, record: record) -// } -// -// do { -// try coreData.context.save() -// } catch { -// Embrace.logger.error("Error saving metadata core data!:\n\(error.localizedDescription)") -// } -// } -// } catch { -// Embrace.logger.error("Error cloning metadata!:\n\(error.localizedDescription)") -// } + guard let coreData = coreData, + let storage = storage else { + return + } + + let request = NSFetchRequest(entityName: MetadataRecordTmp.entityName) + let oldRecords = coreData.fetch(withRequest: request) + + for record in oldRecords { + guard let type = MetadataRecordType(rawValue: record.type), + let lifespan = MetadataRecordLifespan(rawValue: record.lifespan) else { + continue + } + + storage.addMetadata(key: record.key, value: record.value, type: type, lifespan: lifespan) + } + + coreData.destroy() } } diff --git a/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift b/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift index 5119b506..ad088f72 100644 --- a/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift +++ b/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift @@ -196,13 +196,7 @@ class UnsentDataHandler { closeOpenSpans(storage: storage, currentSessionId: currentSessionId) // fetch all sessions in the storage - var sessions: [SessionRecord] - do { - sessions = try storage.fetchAll() - } catch { - Embrace.logger.warning("Error fetching unsent sessions:\n\(error.localizedDescription)") - return - } + let sessions: [SessionRecord] = storage.fetchAll() for session in sessions { // ignore current session @@ -243,61 +237,43 @@ class UnsentDataHandler { upload.uploadSpans(id: session.idRaw, data: payloadData) { result in switch result { case .success: - do { - // remove session from storage - // we can remove this immediately because the upload module will cache it until the upload succeeds - storage.delete(session) + // remove session from storage + // we can remove this immediately because the upload module will cache it until the upload succeeds + storage.delete(session) - if performCleanUp { - cleanOldSpans(storage: storage) - cleanMetadata(storage: storage) - } - - } catch { - Embrace.logger.debug("Error trying to remove session \(session.id):\n\(error.localizedDescription)") + if performCleanUp { + cleanOldSpans(storage: storage) + cleanMetadata(storage: storage) } case .failure(let error): - Embrace.logger.warning("Error trying to upload session \(session.id):\n\(error.localizedDescription)") + Embrace.logger.warning("Error trying to upload session \(session.idRaw):\n\(error.localizedDescription)") } } } static private func cleanOldSpans(storage: EmbraceStorage) { - do { - // first we delete any span record that is closed and its older - // than the oldest session we have on storage - // since spans are only sent when included in a session - // all of these would never be sent anymore, so they can be safely removed - // if no session is found, all closed spans can be safely removed as well - let oldestSession = try storage.fetchOldestSession() - try storage.cleanUpSpans(date: oldestSession?.startTime) - - } catch { - Embrace.logger.warning("Error cleaning old spans:\n\(error.localizedDescription)") - } + // first we delete any span record that is closed and its older + // than the oldest session we have on storage + // since spans are only sent when included in a session + // all of these would never be sent anymore, so they can be safely removed + // if no session is found, all closed spans can be safely removed as well + let oldestSession = storage.fetchOldestSession() + storage.cleanUpSpans(date: oldestSession?.startTime) } static private func closeOpenSpans(storage: EmbraceStorage, currentSessionId: SessionIdentifier?) { - do { - // then we need to close any remaining open spans - // we use the latest session on storage to determine the `endTime` - // since we need to have a valid `endTime` for these spans, we default - // to `Date()` if we don't have a session - let latestSession = try storage.fetchLatestSession(ignoringCurrentSessionId: currentSessionId) - let endTime = (latestSession?.endTime ?? latestSession?.lastHeartbeatTime) ?? Date() - try storage.closeOpenSpans(endTime: endTime) - } catch { - Embrace.logger.warning("Error closing open spans:\n\(error.localizedDescription)") - } + // then we need to close any remaining open spans + // we use the latest session on storage to determine the `endTime` + // since we need to have a valid `endTime` for these spans, we default + // to `Date()` if we don't have a session + let latestSession = storage.fetchLatestSession(ignoringCurrentSessionId: currentSessionId) + let endTime = (latestSession?.endTime ?? latestSession?.lastHeartbeatTime) ?? Date() + storage.closeOpenSpans(endTime: endTime) } static private func cleanMetadata(storage: EmbraceStorage, currentSessionId: String? = nil) { - do { - let sessionId = currentSessionId ?? Embrace.client?.currentSessionId() - try storage.cleanMetadata(currentSessionId: sessionId, currentProcessId: ProcessIdentifier.current.hex) - } catch { - Embrace.logger.warning("Error cleaning up metadata:\n\(error.localizedDescription)") - } + let sessionId = currentSessionId ?? Embrace.client?.currentSessionId() + storage.cleanMetadata(currentSessionId: sessionId, currentProcessId: ProcessIdentifier.current.hex) } } diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index 62925613..073a56eb 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -36,6 +36,7 @@ public class CoreDataWrapper { case let .onDisk(_, baseURL): try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) let description = NSPersistentStoreDescription() + description.type = NSSQLiteStoreType description.url = options.storageMechanism.fileURL self.container.persistentStoreDescriptions = [description] } @@ -50,6 +51,23 @@ public class CoreDataWrapper { self.context.persistentStoreCoordinator = self.container.persistentStoreCoordinator } + /// Removes the database file + public func destroy() { + switch options.storageMechanism { + case .onDisk: + if let url = options.storageMechanism.fileURL { + do { + try container.persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType) + try FileManager.default.removeItem(at: url) + } catch { + logger.error("Error destroying CoreData stack!:\n\(error.localizedDescription)") + } + } + + default: return + } + } + /// Asynchronously saves all changes on the current context to disk public func save() { context.perform { [weak self] in diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage.swift b/Sources/EmbraceStorageInternal/EmbraceStorage.swift index c561579c..b3a8245c 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage.swift @@ -11,7 +11,7 @@ public typealias Storage = EmbraceStorageMetadataFetcher & LogRepository /// Class in charge of storing all the data captured by the Embrace SDK. /// It provides an abstraction layer over a CoreData database. -public class EmbraceStorage: Storage { +public class EmbraceStorage: Storage { public private(set) var options: Options public private(set) var logger: InternalLogger public private(set) var coreData: CoreDataWrapper diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift index 0e645552..fd315c38 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift @@ -28,7 +28,7 @@ extension EmbraceStorage { severity: LogSeverity, body: String, timestamp: Date, - attributes: [String : OpenTelemetryApi.AttributeValue] + attributes: [String: OpenTelemetryApi.AttributeValue] ) -> LogRecord { return LogRecord.create( context: coreData.context, diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift index 2aa873a0..2d93e065 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift @@ -6,12 +6,12 @@ import Foundation import EmbraceCommonInternal public protocol EmbraceStorageMetadataFetcher: AnyObject { - func fetchAllResources() throws -> [MetadataRecord] - func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] - func fetchResourcesForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] - func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] - func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] - func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] + func fetchAllResources() -> [MetadataRecord] + func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] + func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] + func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] + func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] + func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] } extension EmbraceStorage { @@ -25,7 +25,7 @@ extension EmbraceStorage { type: MetadataRecordType, lifespan: MetadataRecordLifespan, lifespanId: String = "" - ) throws -> MetadataRecord? { + ) -> MetadataRecord? { // update existing? if let metadata = updateMetadata( @@ -57,7 +57,6 @@ extension EmbraceStorage { return metadata } - /// Returns the `MetadataRecord` for the given values. public func fetchMetadata( key: String, @@ -81,6 +80,7 @@ extension EmbraceStorage { /// Updates the `MetadataRecord` for the given key, type and lifespan with a new given value. /// - Returns: The updated record, if any + @discardableResult public func updateMetadata( key: String, value: String, @@ -132,7 +132,7 @@ extension EmbraceStorage { type: MetadataRecordType, lifespan: MetadataRecordLifespan, lifespanId: String - ) throws { + ) { guard let metadata = fetchMetadata(key: key, type: type, lifespan: lifespan, lifespanId: lifespanId) else { return @@ -195,7 +195,7 @@ extension EmbraceStorage { } /// Returns all records with types `.requiredResource` or `.resource` - public func fetchAllResources() throws -> [MetadataRecord] { + public func fetchAllResources() -> [MetadataRecord] { let request = MetadataRecord.createFetchRequest() request.predicate = NSPredicate( format: "typeRaw == %@ OR typeRaw == %@", @@ -216,7 +216,7 @@ extension EmbraceStorage { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( type: .and, - subpredicates:[ + subpredicates: [ resourcePredicate(), lifespanPredicate(session: session) ] @@ -231,7 +231,7 @@ extension EmbraceStorage { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( type: .and, - subpredicates:[ + subpredicates: [ resourcePredicate(), lifespanPredicate(processId: processId) ] @@ -249,7 +249,7 @@ extension EmbraceStorage { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( type: .and, - subpredicates:[ + subpredicates: [ customPropertyPredicate(), lifespanPredicate(session: session) ] @@ -267,7 +267,7 @@ extension EmbraceStorage { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( type: .and, - subpredicates:[ + subpredicates: [ personaTagPredicate(), lifespanPredicate(session: session) ] @@ -282,7 +282,7 @@ extension EmbraceStorage { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( type: .and, - subpredicates:[ + subpredicates: [ personaTagPredicate(), lifespanPredicate(processId: processId) ] diff --git a/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift index 7ae4e13c..be986152 100644 --- a/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift @@ -50,9 +50,9 @@ public class LogAttributeRecord: NSManagedObject { private static func typeForValue(_ value: AttributeValue) -> LogAttributeType { switch value { - case .int(_): return .int - case .double(_): return .double - case .bool(_): return .bool + case .int: return .int + case .double: return .double + case .bool: return .bool default: return .string } } From 06d46f39eb850fea9ba9ca69d3342fdacf34e95e Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Tue, 4 Feb 2025 13:57:05 -0300 Subject: [PATCH 11/17] WIP --- .../Logs/EmbraceLogAttributesBuilder.swift | 2 +- .../Logs/Exporter/DefaultLogBatcher.swift | 2 +- .../Payload/Builders/LogPayloadBuilder.swift | 2 +- .../Session/SessionController.swift | 2 +- .../CoreDataWrapper.swift | 10 + .../EmbraceStorage.swift | 2 +- .../Records/EmbraceStorage+Log.swift | 15 +- .../Records/EmbraceStorage+Session.swift | 17 +- .../Cache/EmbraceUploadCache.swift | 4 +- .../EmbraceUploadInternal/EmbraceUpload.swift | 65 ++- .../CoreDataWrapperTests.swift | 33 ++ .../NetworkPayloadCaptureHandlerTests.swift | 2 +- .../AppInfoCaptureServiceTests.swift | 41 +- .../DeviceInfoCaptureServiceTests.swift | 55 ++- .../Capture/ResourceCaptureServiceTests.swift | 22 +- .../Embrace+OTelIntegrationTests.swift | 82 ---- .../EmbraceIntegrationTests.swift | 58 --- .../SpanStorageIntegrationTests.swift | 94 ---- .../Internal/DefaultInternalLoggerTests.swift | 27 +- .../EmbraceSpanProcessor+StorageTests.swift | 5 +- .../DeviceIdentifier+PersistenceTests.swift | 16 +- .../Exporter/DefaultLogBatcherTests.swift | 25 +- .../StorageEmbraceLogExporterTests.swift | 8 +- .../Internal/Logs/LogControllerTests.swift | 23 +- .../Internal/Logs/LogsBatchTests.swift | 8 +- .../ResourceStorageExporterTests.swift | 6 +- .../Internal/StorageSpanExporterTests.swift | 12 +- .../Payload/LogPayloadBuilderTests.swift | 32 +- .../Payload/PayloadUtilTests.swift | 6 +- .../Payload/SessionPayloadBuilderTests.swift | 23 +- .../Payload/SpansPayloadBuilderTests.swift | 16 +- .../Public/EmbraceCoreTests.swift | 38 +- .../MetadataHandler+PersonaTagTests.swift | 81 ++-- .../Metadata/MetadataHandler+UserTests.swift | 2 +- .../Metadata/MetadataHandlerTests.swift | 66 ++- .../Session/SessionControllerTests.swift | 32 +- .../Session/SessionSpanUtilsTests.swift | 16 +- .../Session/UnsentDataHandlerTests.swift | 253 +++++------ .../TestSupport/Records/LogRecord+Init.swift | 46 ++ .../Records/MetadataRecord+Init.swift | 29 ++ .../Records/SessionRecord+Init.swift | 39 ++ .../TestDoubles/MockMetadataFetcher.swift | 12 +- .../TestDoubles/MockSessionController.swift | 5 +- .../TestDoubles/SpyLogRepository.swift | 33 +- .../TestSupport/TestDoubles/SpyStorage.swift | 73 ++-- .../Utilities/MetadataRecord+Factory.swift | 16 +- .../Utilities/SessionRecord+Factory.swift | 4 +- .../Processor/SingleSpanProcessorTests.swift | 2 +- .../EmbraceStorageLoggingTests.swift | 158 ++----- .../EmbraceStorageOptionsTests.swift | 28 -- .../EmbraceStorageTests.swift | 245 +---------- ...aceStorage+SpanForSessionRecordTests.swift | 103 +++-- .../MetadataRecordAttributeValueTests.swift | 191 -------- .../MetadataRecordTests.swift | 409 ++++++------------ .../Migration/MigrationServiceTests.swift | 172 -------- .../Migration/MigrationTests.swift | 43 -- ...40509_00_AddSpanRecordMigrationTests.swift | 123 ------ ...10_00_AddSessionRecordMigrationTests.swift | 143 ------ ...0_01_AddMetadataRecordMigrationTests.swift | 99 ----- ...240510_02_AddLogRecordMigrationTests.swift | 94 ---- ...IdentifierToSpanRecordMigrationTests.swift | 222 ---------- .../Migrations/Migrations+CurrentTests.swift | 26 -- .../Records/LogRecord/LogRecordTests.swift | 84 ---- .../SpanRecord/EmbraceStorage+SpanTests.swift | 71 ++- .../SessionRecordTests.swift | 201 +-------- .../SpanRecordTests.swift | 216 ++------- .../TestSupport/Migration+Helpers.swift | 23 - .../TestSupport/ThrowingMigration.swift | 37 -- ...mbraceUploadCacheTests+ClearDataDate.swift | 14 +- .../EmbraceUploadCacheTests.swift | 2 +- .../Extensions/EmbraceStorage+Extension.swift | 18 +- 71 files changed, 998 insertions(+), 3186 deletions(-) delete mode 100644 Tests/EmbraceCoreTests/IntegrationTests/Embrace+OTelIntegrationTests.swift delete mode 100644 Tests/EmbraceCoreTests/IntegrationTests/EmbraceIntegrationTests.swift delete mode 100644 Tests/EmbraceCoreTests/IntegrationTests/EmbraceOTelStorageIntegration/SpanStorageIntegrationTests.swift create mode 100644 Tests/EmbraceCoreTests/TestSupport/Records/LogRecord+Init.swift create mode 100644 Tests/EmbraceCoreTests/TestSupport/Records/MetadataRecord+Init.swift create mode 100644 Tests/EmbraceCoreTests/TestSupport/Records/SessionRecord+Init.swift delete mode 100644 Tests/EmbraceStorageInternalTests/EmbraceStorageOptionsTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/MetadataRecordAttributeValueTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Migration/MigrationServiceTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Migration/MigrationTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Migration/Migrations/20240509_00_AddSpanRecordMigrationTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_00_AddSessionRecordMigrationTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_01_AddMetadataRecordMigrationTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_02_AddLogRecordMigrationTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigrationTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Migration/Migrations/Migrations+CurrentTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/Records/LogRecord/LogRecordTests.swift delete mode 100644 Tests/EmbraceStorageInternalTests/TestSupport/Migration+Helpers.swift delete mode 100644 Tests/EmbraceStorageInternalTests/TestSupport/ThrowingMigration.swift diff --git a/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift b/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift index df312f5a..4e8c7172 100644 --- a/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift +++ b/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift @@ -81,7 +81,7 @@ class EmbraceLogAttributesBuilder { let key = String(format: LogSemantics.keyPropertiesPrefix, record.key) attributes[key] = record.value } - + return self } diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift index 794649e6..47f0434c 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift @@ -41,7 +41,7 @@ class DefaultLogBatcher: LogBatcher { func addLogRecord(logRecord: ReadableLogRecord) { processorQueue.async { - let record = self.repository.create( + let record = self.repository.createLog( id: LogIdentifier(), processId: ProcessIdentifier.current, severity: logRecord.severity?.toLogSeverity() ?? .info, diff --git a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift index 9edc8ac3..21c3733d 100644 --- a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift @@ -10,7 +10,7 @@ import EmbraceSemantics struct LogPayloadBuilder { static func build(log: LogRecord) -> LogPayload { var finalAttributes: [Attribute] = log.attributes.map { entry in - Attribute(key: entry.key, value: entry.value.description) + Attribute(key: entry.key, value: entry.valueRaw) } finalAttributes.append(.init(key: LogSemantics.keyId, value: log.idRaw)) diff --git a/Sources/EmbraceCore/Session/SessionController.swift b/Sources/EmbraceCore/Session/SessionController.swift index c89baeaa..026f5884 100644 --- a/Sources/EmbraceCore/Session/SessionController.swift +++ b/Sources/EmbraceCore/Session/SessionController.swift @@ -131,8 +131,8 @@ class SessionController: SessionControllable { // create session record let session = storage.addSession( id: newId, - state: state, processId: ProcessIdentifier.current, + state: state, traceId: span.context.traceId.hexString, spanId: span.context.spanId.hexString, startTime: startTime diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index 073a56eb..59e0d3a3 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -53,6 +53,8 @@ public class CoreDataWrapper { /// Removes the database file public func destroy() { + context.reset() + switch options.storageMechanism { case .onDisk: if let url = options.storageMechanism.fileURL { @@ -66,6 +68,14 @@ public class CoreDataWrapper { default: return } + + if let store = container.persistentStoreCoordinator.persistentStores.first { + do { + try container.persistentStoreCoordinator.remove(store) + } catch { + logger.error("Error removing CoreData store!:\n\(error.localizedDescription)") + } + } } /// Asynchronously saves all changes on the current context to disk diff --git a/Sources/EmbraceStorageInternal/EmbraceStorage.swift b/Sources/EmbraceStorageInternal/EmbraceStorage.swift index b3a8245c..1af90aab 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage.swift @@ -33,7 +33,7 @@ public class EmbraceStorage: Storage { var entities: [NSEntityDescription] = [ SessionRecord.entityDescription, SpanRecord.entityDescription, - MetadataRecord.entityDescription + MetadataRecord.entityDescription, ] entities.append(contentsOf: LogRecord.entityDescriptions) diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift index fd315c38..a1233d68 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift @@ -7,7 +7,7 @@ import EmbraceCommonInternal import OpenTelemetryApi public protocol LogRepository { - func create( + func createLog( id: LogIdentifier, processId: ProcessIdentifier, severity: LogSeverity, @@ -22,12 +22,13 @@ public protocol LogRepository { extension EmbraceStorage { - public func create( + @discardableResult + public func createLog( id: LogIdentifier, processId: ProcessIdentifier, severity: LogSeverity, body: String, - timestamp: Date, + timestamp: Date = Date(), attributes: [String: OpenTelemetryApi.AttributeValue] ) -> LogRecord { return LogRecord.create( @@ -49,15 +50,11 @@ extension EmbraceStorage { } public func removeAllLogs() { - remove(logs: getAll()) + let logs: [LogRecord] = fetchAll() + remove(logs: logs) } public func remove(logs: [LogRecord]) { coreData.deleteRecords(logs) } - - public func getAll() -> [LogRecord] { - let request = LogRecord.createFetchRequest() - return coreData.fetch(withRequest: request) - } } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift index 82d348b3..cd094dc0 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift @@ -9,8 +9,8 @@ extension EmbraceStorage { /// Adds a session to the storage synchronously. /// - Parameters: /// - id: Identifier of the session - /// - state: `SessionState` of the session /// - processId: `ProcessIdentifier` of the session + /// - state: `SessionState` of the session /// - traceId: String representing the trace identifier of the corresponding session span /// - spanId: String representing the span identifier of the corresponding session span /// - startTime: `Date` of when the session started @@ -21,14 +21,17 @@ extension EmbraceStorage { @discardableResult public func addSession( id: SessionIdentifier, - state: SessionState, processId: ProcessIdentifier, + state: SessionState, traceId: String, spanId: String, startTime: Date, endTime: Date? = nil, lastHeartbeatTime: Date? = nil, - crashReportId: String? = nil + crashReportId: String? = nil, + coldStart: Bool = false, + cleanExit: Bool = false, + appTerminated: Bool = false ) -> SessionRecord { // update existing? @@ -40,6 +43,9 @@ extension EmbraceStorage { session.startTime = startTime session.endTime = endTime session.crashReportId = crashReportId + session.coldStart = coldStart + session.cleanExit = cleanExit + session.appTerminated = appTerminated if let lastHeartbeatTime = lastHeartbeatTime { session.lastHeartbeatTime = lastHeartbeatTime @@ -59,7 +65,10 @@ extension EmbraceStorage { spanId: spanId, startTime: startTime, endTime: endTime, - lastHeartbeatTime: lastHeartbeatTime + lastHeartbeatTime: lastHeartbeatTime, + coldStart: coldStart, + cleanExit: cleanExit, + appTerminated: appTerminated ) coreData.save() diff --git a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift index 66061901..19c8fb21 100644 --- a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift +++ b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift @@ -46,13 +46,13 @@ class EmbraceUploadCache { /// Fetches all the cached upload data. /// - Returns: An array containing all the cached `UploadDataRecords` - public func fetchAllUploadData() throws -> [UploadDataRecord] { + public func fetchAllUploadData() -> [UploadDataRecord] { let request = NSFetchRequest(entityName: UploadDataRecord.entityName) return coreData.fetch(withRequest: request) } /// Removes stale data based on size or date, if they're limited in options. - @discardableResult public func clearStaleDataIfNeeded() throws -> UInt { + @discardableResult public func clearStaleDataIfNeeded() -> UInt { guard options.cacheDaysLimit > 0 else { return 0 } diff --git a/Sources/EmbraceUploadInternal/EmbraceUpload.swift b/Sources/EmbraceUploadInternal/EmbraceUpload.swift index 1ee6a1a4..800b279e 100644 --- a/Sources/EmbraceUploadInternal/EmbraceUpload.swift +++ b/Sources/EmbraceUploadInternal/EmbraceUpload.swift @@ -65,45 +65,42 @@ public class EmbraceUpload: EmbraceLogUploader { public func retryCachedData() { queue.async { [weak self] in guard let self = self else { return } - do { - // in place mechanism to not retry sending cache data at the same time - guard !self.isRetryingCache else { - return - } - self.isRetryingCache = true + // in place mechanism to not retry sending cache data at the same time + guard !self.isRetryingCache else { + return + } - defer { - // on finishing everything, allow to retry cache (i.e. reconnection) - self.isRetryingCache = false - } + self.isRetryingCache = true + + defer { + // on finishing everything, allow to retry cache (i.e. reconnection) + self.isRetryingCache = false + } - // clear data from cache that shouldn't be retried as it's stale - self.clearCacheFromStaleData() + // clear data from cache that shouldn't be retried as it's stale + self.clearCacheFromStaleData() - // get all the data cached first, is the only thing that could throw - let cachedObjects = try self.cache.fetchAllUploadData() + // get all the data cached first, is the only thing that could throw + let cachedObjects = self.cache.fetchAllUploadData() - // create a sempahore to allow only to send two request at a time so we don't - // get throttled by the backend on cases where cache has many failed requests. + // create a sempahore to allow only to send two request at a time so we don't + // get throttled by the backend on cases where cache has many failed requests. - for uploadData in cachedObjects { - guard let type = EmbraceUploadType(rawValue: uploadData.type) else { - continue - } - self.semaphore.wait() + for uploadData in cachedObjects { + guard let type = EmbraceUploadType(rawValue: uploadData.type) else { + continue + } + self.semaphore.wait() - self.reUploadData( - id: uploadData.id, - data: uploadData.data, - type: type, - attemptCount: uploadData.attemptCount - ) { - self.semaphore.signal() - } + self.reUploadData( + id: uploadData.id, + data: uploadData.data, + type: type, + attemptCount: uploadData.attemptCount + ) { + self.semaphore.signal() } - } catch { - self.logger.debug("Error retrying cached upload data: \(error.localizedDescription)") } } } @@ -323,11 +320,7 @@ public class EmbraceUpload: EmbraceLogUploader { private func clearCacheFromStaleData() { operationQueue.addOperation { [weak self] in - do { - try self?.cache.clearStaleDataIfNeeded() - } catch { - self?.logger.debug("Error clearing stale date from cache: \(error.localizedDescription)") - } + self?.cache.clearStaleDataIfNeeded() } } diff --git a/Tests/EmbraceCoreDataInternalTests/CoreDataWrapperTests.swift b/Tests/EmbraceCoreDataInternalTests/CoreDataWrapperTests.swift index 51d81da0..4eb8c343 100644 --- a/Tests/EmbraceCoreDataInternalTests/CoreDataWrapperTests.swift +++ b/Tests/EmbraceCoreDataInternalTests/CoreDataWrapperTests.swift @@ -19,6 +19,25 @@ class CoreDataWrapperTests: XCTestCase { try wrapper = CoreDataWrapper(options: options, logger: MockLogger()) } + func test_destroy() throws { + // given a wrapper with data on disk + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + let storageMechanism: StorageMechanism = .onDisk(name: testName, baseURL: url) + let options = CoreDataWrapper.Options(storageMechanism: storageMechanism, entities: [MockRecord.entityDescription]) + try wrapper = CoreDataWrapper(options: options, logger: MockLogger()) + + _ = MockRecord.create(context: wrapper.context, id: "test") + wrapper.save() + + XCTAssert(FileManager.default.fileExists(atPath: storageMechanism.fileURL!.path)) + + // when destroying the stack + wrapper.destroy() + + // then the db file is removed + XCTAssertFalse(FileManager.default.fileExists(atPath: storageMechanism.fileURL!.path)) + } + func test_fetch() throws { // given a wrapper with data _ = MockRecord.create(context: wrapper.context, id: "test") @@ -33,7 +52,21 @@ class CoreDataWrapperTests: XCTestCase { // then the data is correct XCTAssertEqual(result.count, 1) XCTAssertEqual(result.first!.id, "test") + } + + func test_count() throws { + // given a wrapper with data + _ = MockRecord.create(context: wrapper.context, id: "test1") + _ = MockRecord.create(context: wrapper.context, id: "test2") + _ = MockRecord.create(context: wrapper.context, id: "test3") + wrapper.save() + + // when fetching count + let request = NSFetchRequest(entityName: MockRecord.entityName) + let result = wrapper.count(withRequest: request) + // then the data is correct + XCTAssertEqual(result, 3) } func test_deleteRecord() throws { diff --git a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift index 20a0ca59..cecb453d 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift @@ -55,8 +55,8 @@ class NetworkPayloadCaptureHandlerTests: XCTestCase { // when a session starts let session = SessionRecord( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date() diff --git a/Tests/EmbraceCoreTests/Capture/OneTimeServices/AppInfoCaptureServiceTests.swift b/Tests/EmbraceCoreTests/Capture/OneTimeServices/AppInfoCaptureServiceTests.swift index 563652d5..8d5899e1 100644 --- a/Tests/EmbraceCoreTests/Capture/OneTimeServices/AppInfoCaptureServiceTests.swift +++ b/Tests/EmbraceCoreTests/Capture/OneTimeServices/AppInfoCaptureServiceTests.swift @@ -13,7 +13,7 @@ final class AppInfoCaptureServiceTests: XCTestCase { func test_started() throws { // given an app info capture service let service = AppInfoCaptureService() - let handler = try EmbraceStorage.createInDiskDb() + let handler = try EmbraceStorage.createInDiskDb(fileName: testName) service.handler = handler // when the service is installed and started @@ -24,92 +24,87 @@ final class AppInfoCaptureServiceTests: XCTestCase { let processId = ProcessIdentifier.current.hex // bundle version - let bundleVersion = try handler.fetchMetadata( + let bundleVersion = handler.fetchMetadata( key: AppResourceKey.bundleVersion.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(bundleVersion) - XCTAssertEqual(bundleVersion!.stringValue, EMBDevice.bundleVersion) + XCTAssertEqual(bundleVersion!.value, EMBDevice.bundleVersion) // environment - let environment = try handler.fetchMetadata( + let environment = handler.fetchMetadata( key: AppResourceKey.environment.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(environment) - XCTAssertEqual(environment!.stringValue, EMBDevice.environment) + XCTAssertEqual(environment!.value, EMBDevice.environment) // environment detail - let environmentDetail = try handler.fetchMetadata( + let environmentDetail = handler.fetchMetadata( key: AppResourceKey.detailedEnvironment.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(environmentDetail) - XCTAssertEqual(environmentDetail!.stringValue, EMBDevice.environmentDetail) + XCTAssertEqual(environmentDetail!.value, EMBDevice.environmentDetail) // framework - let framework = try handler.fetchMetadata( + let framework = handler.fetchMetadata( key: AppResourceKey.framework.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(framework) - XCTAssertEqual(framework!.integerValue, -1) + XCTAssertEqual(framework!.value, "-1") // sdk version - let sdkVersion = try handler.fetchMetadata( + let sdkVersion = handler.fetchMetadata( key: AppResourceKey.sdkVersion.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(sdkVersion) - XCTAssertEqual(sdkVersion!.stringValue, EmbraceMeta.sdkVersion) + XCTAssertEqual(sdkVersion!.value, EmbraceMeta.sdkVersion) // app version - let appVersion = try handler.fetchMetadata( + let appVersion = handler.fetchMetadata( key: AppResourceKey.appVersion.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(appVersion) - XCTAssertEqual(appVersion!.stringValue, EMBDevice.appVersion) + XCTAssertEqual(appVersion!.value, EMBDevice.appVersion) // process identifier - let processIdentifier = try handler.fetchMetadata( + let processIdentifier = handler.fetchMetadata( key: AppResourceKey.processIdentifier.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(processIdentifier) - XCTAssertEqual(processIdentifier!.stringValue, ProcessIdentifier.current.hex) + XCTAssertEqual(processIdentifier!.value, ProcessIdentifier.current.hex) } func test_notStarted() throws { // given an app info capture service let service = AppInfoCaptureService() - let handler = try EmbraceStorage.createInDiskDb() + let handler = try EmbraceStorage.createInDiskDb(fileName: testName) service.handler = handler // when the service is installed but not started service.install(otel: nil) // then no resources are captured - let expectation = XCTestExpectation() - try handler.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), 0) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let metadata: [MetadataRecord] = handler.fetchAll() + XCTAssertEqual(metadata.count, 0) } } diff --git a/Tests/EmbraceCoreTests/Capture/OneTimeServices/DeviceInfoCaptureServiceTests.swift b/Tests/EmbraceCoreTests/Capture/OneTimeServices/DeviceInfoCaptureServiceTests.swift index 8a64f768..46a98ae5 100644 --- a/Tests/EmbraceCoreTests/Capture/OneTimeServices/DeviceInfoCaptureServiceTests.swift +++ b/Tests/EmbraceCoreTests/Capture/OneTimeServices/DeviceInfoCaptureServiceTests.swift @@ -24,81 +24,81 @@ final class DeviceInfoCaptureServiceTests: XCTestCase { // then the app info resources are correctly stored let processId = ProcessIdentifier.current.hex - let resources = try handler.fetchResourcesForProcessId(.current) + let resources = handler.fetchResourcesForProcessId(.current) XCTAssertEqual(resources.count, 11) // jailbroken - let jailbroken = try handler.fetchMetadata( + let jailbroken = handler.fetchMetadata( key: DeviceResourceKey.isJailbroken.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(jailbroken) - XCTAssertEqual(jailbroken!.stringValue, "false") + XCTAssertEqual(jailbroken!.value, "false") // locale - let locale = try handler.fetchMetadata( + let locale = handler.fetchMetadata( key: DeviceResourceKey.locale.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(locale) - XCTAssertEqual(locale!.stringValue, EMBDevice.locale) + XCTAssertEqual(locale!.value, EMBDevice.locale) // timezone - let timezone = try handler.fetchMetadata( + let timezone = handler.fetchMetadata( key: DeviceResourceKey.timezone.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(timezone) - XCTAssertEqual(timezone!.stringValue, EMBDevice.timezoneDescription) + XCTAssertEqual(timezone!.value, EMBDevice.timezoneDescription) // disk space - let diskSpace = try handler.fetchMetadata( + let diskSpace = handler.fetchMetadata( key: DeviceResourceKey.totalDiskSpace.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(diskSpace) - XCTAssertEqual(diskSpace!.integerValue, EMBDevice.totalDiskSpace.intValue) + XCTAssertEqual(diskSpace!.value, String(EMBDevice.totalDiskSpace.intValue)) // os version - let osVersion = try handler.fetchMetadata( + let osVersion = handler.fetchMetadata( key: ResourceAttributes.osVersion.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(osVersion) - XCTAssertEqual(osVersion!.stringValue, EMBDevice.operatingSystemVersion) + XCTAssertEqual(osVersion!.value, EMBDevice.operatingSystemVersion) // os build - let osBuild = try handler.fetchMetadata( + let osBuild = handler.fetchMetadata( key: DeviceResourceKey.osBuild.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(osBuild) - XCTAssertEqual(osBuild!.stringValue, EMBDevice.operatingSystemBuild) + XCTAssertEqual(osBuild!.value, EMBDevice.operatingSystemBuild) // os variant - let osVariant = try handler.fetchMetadata( + let osVariant = handler.fetchMetadata( key: DeviceResourceKey.osVariant.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(osVariant) - XCTAssertEqual(osVariant!.stringValue, EMBDevice.operatingSystemType) + XCTAssertEqual(osVariant!.value, EMBDevice.operatingSystemType) // model - let model = try handler.fetchMetadata( + let model = handler.fetchMetadata( key: ResourceAttributes.deviceModelIdentifier.rawValue, type: .requiredResource, lifespan: .process, @@ -106,37 +106,37 @@ final class DeviceInfoCaptureServiceTests: XCTestCase { ) XCTAssertNotNil(model) - XCTAssertEqual(model!.stringValue, EMBDevice.model) + XCTAssertEqual(model!.value, EMBDevice.model) // osType - let osType = try handler.fetchMetadata( + let osType = handler.fetchMetadata( key: ResourceAttributes.osType.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(osType) - XCTAssertEqual(try XCTUnwrap(osType?.stringValue), "darwin") + XCTAssertEqual(osType!.value, "darwin") // osName - let osName = try handler.fetchMetadata( + let osName = handler.fetchMetadata( key: ResourceAttributes.osName.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(osName) - XCTAssertEqual(try XCTUnwrap(osName?.stringValue), EMBDevice.operatingSystemType) + XCTAssertEqual(osName!.value, EMBDevice.operatingSystemType) // osName - let architecture = try handler.fetchMetadata( + let architecture = handler.fetchMetadata( key: DeviceResourceKey.architecture.rawValue, type: .requiredResource, lifespan: .process, lifespanId: processId ) XCTAssertNotNil(architecture) - XCTAssertEqual(try XCTUnwrap(architecture?.stringValue), EMBDevice.architecture) + XCTAssertEqual(architecture!.value, EMBDevice.architecture) } func test_notStarted() throws { @@ -149,12 +149,7 @@ final class DeviceInfoCaptureServiceTests: XCTestCase { service.install(otel: nil) // then no resources are captured - let expectation = XCTestExpectation() - try handler.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), 0) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let metadata: [MetadataRecord] = handler.fetchAll() + XCTAssertEqual(metadata.count, 0) } } diff --git a/Tests/EmbraceCoreTests/Capture/ResourceCaptureServiceTests.swift b/Tests/EmbraceCoreTests/Capture/ResourceCaptureServiceTests.swift index b8e8028c..7b26b793 100644 --- a/Tests/EmbraceCoreTests/Capture/ResourceCaptureServiceTests.swift +++ b/Tests/EmbraceCoreTests/Capture/ResourceCaptureServiceTests.swift @@ -21,20 +21,12 @@ class ResourceCaptureServiceTests: XCTestCase { service.addResource(key: "test", value: .string("value")) // then the resource is added to the storage - let expectation = XCTestExpectation() - try handler.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), 1) - - let record = try MetadataRecord.fetchOne(db) - XCTAssertEqual(record!.key, "test") - XCTAssertEqual(record!.value, .string("value")) - XCTAssertEqual(record!.type, .requiredResource) - XCTAssertEqual(record!.lifespan, .process) - XCTAssertEqual(record!.lifespanId, ProcessIdentifier.current.hex) - - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let metadata: [MetadataRecord] = handler.fetchAll() + XCTAssertEqual(metadata.count, 1) + XCTAssertEqual(metadata[0].key, "test") + XCTAssertEqual(metadata[0].value, "value") + XCTAssertEqual(metadata[0].type, .requiredResource) + XCTAssertEqual(metadata[0].lifespan, .process) + XCTAssertEqual(metadata[0].lifespanId, ProcessIdentifier.current.hex) } } diff --git a/Tests/EmbraceCoreTests/IntegrationTests/Embrace+OTelIntegrationTests.swift b/Tests/EmbraceCoreTests/IntegrationTests/Embrace+OTelIntegrationTests.swift deleted file mode 100644 index 1a3edb54..00000000 --- a/Tests/EmbraceCoreTests/IntegrationTests/Embrace+OTelIntegrationTests.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest -import EmbraceCore -import EmbraceOTelInternal -import TestSupport - -final class Embrace_OTelIntegrationTests: IntegrationTestCase { - -// MARK: recordCompletedSpan - func test_recordCompletedSpan_setsStatus_toOk() throws { - throw XCTSkip() - - let exporter = InMemorySpanExporter() - let embrace = try Embrace.setup(options: .init( - appId: "myApp", - captureServices: [], - crashReporter: nil, - export: .init(spanExporter: exporter) - )).start() - - embrace.recordCompletedSpan( - name: "my-example-span", - type: .performance, - parent: nil, - startTime: Date(), - endTime: Date(), - attributes: [:], - events: [], - errorCode: nil - ) - - let expectation = expectation(description: "export completes") - exporter.onExportComplete { - if let result = exporter.exportedSpans.values.first(where: { value in - value.name == "my-example-span" - }) { - XCTAssertEqual(result.status, .ok) - expectation.fulfill() - } - } - - wait(for: [expectation], timeout: 6.0) - } - - func test_recordCompletedSpan_withErrorCode_setsStatus_toError() throws { - throw XCTSkip() - - let exporter = InMemorySpanExporter() - let embrace = try Embrace.setup(options: .init( - appId: "myApp", - captureServices: [], - crashReporter: nil, - export: .init(spanExporter: exporter) - )) - - embrace.recordCompletedSpan( - name: "my-example-span", - type: .performance, - parent: nil, - startTime: Date(), - endTime: Date(), - attributes: [:], - events: [], - errorCode: .userAbandon - ) - - let expectation = expectation(description: "export completes") - exporter.onExportComplete { - if let result = exporter.exportedSpans.values.first(where: { value in - value.name == "my-example-span" - }) { - XCTAssertEqual(result.status, .error(description: "user_abandon")) - expectation.fulfill() - } - } - wait(for: [expectation], timeout: 6.0) - } - -} diff --git a/Tests/EmbraceCoreTests/IntegrationTests/EmbraceIntegrationTests.swift b/Tests/EmbraceCoreTests/IntegrationTests/EmbraceIntegrationTests.swift deleted file mode 100644 index 9a92bb8b..00000000 --- a/Tests/EmbraceCoreTests/IntegrationTests/EmbraceIntegrationTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest -@testable import EmbraceCore -import EmbraceStorageInternal -import EmbraceOTelInternal -import GRDB -import TestSupport -import OpenTelemetrySdk - -final class EmbraceIntegrationTests: IntegrationTestCase { - - let options = Embrace.Options(appId: "myApp", captureServices: [], crashReporter: nil) - - // TESTSKIP: This test is flakey in CI. It seems the value observation in the DB is not consistent - // May want to introduce and `Embrace.shutdown` method that will flush the SpanProcessor. - // This will allow us to not need to perform the value observation and also not wait an arbitrary - // amount of time for the spans to be processed/exported. - func skip_test_start_createsProcessLaunchSpan() throws { - var processLaunchSpan: SpanData? - var sdkStartSpan: SpanData? - try Embrace.setup(options: options) - - let expectation = expectation(description: "wait for span records") - let observation = ValueObservation.tracking(SpanRecord.fetchAll) - - let cancellable = observation.start(in: Embrace.client!.storage.dbQueue) { error in - XCTAssert(false, error.localizedDescription) - } onChange: { records in - let spanDatas = (try? records.map { record in - try JSONDecoder().decode(SpanData.self, from: record.data) - }) ?? [] - - if let processLaunch = spanDatas.first(where: { $0.name == "emb-process-launch" }), - let sdkStart = spanDatas.first(where: { $0.name == "emb-sdk-start" }) { - processLaunchSpan = processLaunch - sdkStartSpan = sdkStart - expectation.fulfill() - } - } - - // When - try Embrace.client!.start() - - wait(for: [expectation], timeout: .defaultTimeout) - - XCTAssertNotNil(processLaunchSpan) - XCTAssertNotNil(sdkStartSpan) - XCTAssertNotNil(processLaunchSpan) - XCTAssertNotNil(sdkStartSpan) - XCTAssertEqual(sdkStartSpan?.parentSpanId, processLaunchSpan?.spanId) - XCTAssertEqual(sdkStartSpan?.traceId, processLaunchSpan?.traceId) - - cancellable.cancel() - } -} diff --git a/Tests/EmbraceCoreTests/IntegrationTests/EmbraceOTelStorageIntegration/SpanStorageIntegrationTests.swift b/Tests/EmbraceCoreTests/IntegrationTests/EmbraceOTelStorageIntegration/SpanStorageIntegrationTests.swift deleted file mode 100644 index fa5de546..00000000 --- a/Tests/EmbraceCoreTests/IntegrationTests/EmbraceOTelStorageIntegration/SpanStorageIntegrationTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest - -@testable import EmbraceCore -import EmbraceOTelInternal -import EmbraceStorageInternal -import GRDB - -import TestSupport - -final class SpanStorageIntegrationTests: IntegrationTestCase { - - var storage: EmbraceStorage! - let sdkStateProvider = MockEmbraceSDKStateProvider() - - override func setUpWithError() throws { - storage = try EmbraceStorage.createInMemoryDb() - let exporter = StorageSpanExporter(options: .init(storage: storage), logger: MockLogger()) - - EmbraceOTel.setup(spanProcessors: [SingleSpanProcessor(spanExporter: exporter, sdkStateProvider: sdkStateProvider)]) - } - - override func tearDownWithError() throws { - _ = try storage.dbQueue.inDatabase { db in - try SpanRecord.deleteAll(db) - } - try storage.teardown() - } - - // TESTSKIP: ValueObservation - func skip_test_buildSpan_storesOpenSpan() throws { - let exp = expectation(description: "Observe Insert") - let observation = ValueObservation.tracking(SpanRecord.fetchAll) - let cancellable = observation.start(in: storage.dbQueue) { error in - fatalError("Error: \(error)") - } onChange: { records in - if records.count > 0 { - exp.fulfill() - } - } - - let otel = EmbraceOTel() - _ = otel.buildSpan(name: "example", type: .performance) - .setAttribute(key: "foo", value: "bar") - .startSpan() - wait(for: [exp], timeout: 1.0) - - let records: [SpanRecord] = try storage.fetchAll() - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records.first?.type, .performance) - if let openSpan = records.first { - XCTAssertNil(openSpan.endTime) - } - - cancellable.cancel() - } - - // TESTSKIP: ValueObservation - func skip_test_buildSpan_storesSpanThatEnded() throws { - let exp = expectation(description: "Observe Insert") - let observation = ValueObservation.tracking { db in - try SpanRecord - .filter(Column("end_time") != nil) - .fetchAll(db) - } - let cancellable = observation.start(in: storage.dbQueue) { error in - fatalError("Error: \(error)") - } onChange: { records in - if records.count > 0 { - exp.fulfill() - } - } - - let otel = EmbraceOTel() - let span = otel.buildSpan(name: "example", type: .performance) - .setAttribute(key: "foo", value: "bar") - .startSpan() - - span.end() - wait(for: [exp], timeout: 1.0) - - let records: [SpanRecord] = try storage.fetchAll() - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records.first?.type, .performance) - if let openSpan = records.first { - XCTAssertNotNil(openSpan.endTime) - } - - cancellable.cancel() - } -} diff --git a/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift b/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift index 260758b0..9365377d 100644 --- a/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift @@ -12,14 +12,25 @@ import OpenTelemetryApi class DefaultInternalLoggerTests: XCTestCase { - let session = SessionRecord( - id: TestConstants.sessionId, - state: .foreground, - processId: TestConstants.processId, - traceId: TestConstants.traceId, - spanId: TestConstants.spanId, - startTime: Date() - ) + var session: SessionRecord! + var storage: EmbraceStorage! + + override func setUpWithError() throws { + storage = try EmbraceStorage.createInMemoryDb() + + session = storage.addSession( + id: TestConstants.sessionId, + processId: TestConstants.processId, + state: .foreground, + traceId: TestConstants.traceId, + spanId: TestConstants.spanId, + startTime: Date() + ) + } + + override func tearDownWithError() throws { + storage.coreData.destroy() + } func test_none() { let logger = DefaultInternalLogger() diff --git a/Tests/EmbraceCoreTests/Internal/EmbraceSpanProcessor+StorageTests.swift b/Tests/EmbraceCoreTests/Internal/EmbraceSpanProcessor+StorageTests.swift index 3b383b7e..31334340 100644 --- a/Tests/EmbraceCoreTests/Internal/EmbraceSpanProcessor+StorageTests.swift +++ b/Tests/EmbraceCoreTests/Internal/EmbraceSpanProcessor+StorageTests.swift @@ -15,9 +15,8 @@ final class EmbraceSpanProcessor_StorageTests: XCTestCase { func test_spanProcessor_withStorage_usesStorageExporter() throws { let storage = try EmbraceStorage.createInMemoryDb() - defer { - try? storage.teardown() - } + defer { storage.coreData.destroy() } + let processor = SingleSpanProcessor( spanExporter: StorageSpanExporter( options: .init(storage: storage), diff --git a/Tests/EmbraceCoreTests/Internal/Identifiers/DeviceIdentifier+PersistenceTests.swift b/Tests/EmbraceCoreTests/Internal/Identifiers/DeviceIdentifier+PersistenceTests.swift index ffa182be..164cc535 100644 --- a/Tests/EmbraceCoreTests/Internal/Identifiers/DeviceIdentifier+PersistenceTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Identifiers/DeviceIdentifier+PersistenceTests.swift @@ -17,31 +17,31 @@ class DeviceIdentifier_PersistenceTests: XCTestCase { KeychainAccess.keychain = AlwaysSuccessfulKeychainInterface() // delete the resource if we already have it - if let resource = try storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) { - try storage.delete(record: resource) + if let resource = storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) { + storage.delete(resource) } } override func tearDownWithError() throws { - try storage.teardown() + storage.coreData.destroy() } func test_retrieve_withNoRecordInStorage_shouldCreateNewPermanentRecord() throws { let result = DeviceIdentifier.retrieve(from: storage) - let resourceRecord = try storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) + let resourceRecord = storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) XCTAssertNotNil(resourceRecord) XCTAssertEqual(resourceRecord?.lifespan, .permanent) - let storedDeviceId = try XCTUnwrap(resourceRecord?.uuidValue) + let storedDeviceId = UUID(withoutHyphen: resourceRecord!.value)! XCTAssertEqual(result, DeviceIdentifier(value: storedDeviceId)) } func test_retrieve_withNoRecordInStorage_shouldRequestFromKeychain() throws { // because of our setup we could assume there is no database entry but lets make sure // to delete the resource if we already have it - if let resource = try storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) { - try storage.delete(record: resource) + if let resource = storage.fetchRequiredPermanentResource(key: DeviceIdentifier.resourceKey) { + storage.delete(resource) } let keychainDeviceId = KeychainAccess.deviceId @@ -55,7 +55,7 @@ class DeviceIdentifier_PersistenceTests: XCTestCase { let deviceId = DeviceIdentifier(value: UUID()) - try storage.addMetadata( + storage.addMetadata( key: DeviceIdentifier.resourceKey, value: deviceId.hex, type: .requiredResource, diff --git a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift index 98181342..127198ac 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift @@ -7,6 +7,7 @@ import XCTest @testable import EmbraceCore import EmbraceStorageInternal import TestSupport +import OpenTelemetrySdk class DefaultLogBatcherTests: XCTestCase { private var sut: DefaultLogBatcher! @@ -15,21 +16,18 @@ class DefaultLogBatcherTests: XCTestCase { func test_addLog_alwaysTriesToCreateLogInRepository() { givenDefaultLogBatcher() - givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenLogRepositoryCreateMethodWasInvoked() } func testOnSuccessfulRepository_whenInvokingAddLog_thenBatchShouldntFinish() { givenDefaultLogBatcher() - givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenDelegateShouldntInvokeBatchFinished() } func testOnSuccessfulRepository_whenInvokingAddLogMoreTimesThanLimit_thenBatchShouldFinish() { givenDefaultLogBatcher(limits: .init(maxLogsPerBatch: 1)) - givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenDelegateShouldInvokeBatchFinished() @@ -37,14 +35,12 @@ class DefaultLogBatcherTests: XCTestCase { func testAutoEndBatchAfterLifespanExpired() { givenDefaultLogBatcher(limits: .init(maxBatchAge: 0.1, maxLogsPerBatch: 10)) - givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenDelegateShouldInvokeBatchFinishedAfterBatchLifespan(0.5) } func testAutoEndBatchAfterLifespanExpired_TimerStartsAgainAfterNewLogAdded() { givenDefaultLogBatcher(limits: .init(maxBatchAge: 0.1, maxLogsPerBatch: 10)) - givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) thenDelegateShouldInvokeBatchFinishedAfterBatchLifespan(0.2) self.delegate.didCallBatchFinished = false @@ -54,7 +50,6 @@ class DefaultLogBatcherTests: XCTestCase { func testAutoEndBatchAfterLifespanExpired_CancelWhenBatchEndedPrematurely() { givenDefaultLogBatcher(limits: .init(maxBatchAge: 0.1, maxLogsPerBatch: 3)) - givenRepositoryCreatesLogsSuccessfully() whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) whenInvokingAddLogRecord(withLogRecord: randomLogRecord()) @@ -71,22 +66,16 @@ private extension DefaultLogBatcherTests { sut = .init(repository: repository, logLimits: limits, delegate: delegate, processorQueue: .main) } - func givenRepositoryCreatesLogsSuccessfully(withLog log: LogRecord? = nil) { - let logRecord = log ?? randomLogRecord() - repository.stubbedCreateCompletionResult = .success(logRecord) - } - - func randomLogRecord() -> LogRecord { - .init( - identifier: .init(), - processIdentifier: .random, - severity: .info, - body: UUID().uuidString, + func randomLogRecord() -> ReadableLogRecord { + return ReadableLogRecord( + resource: Resource(), + instrumentationScopeInfo: InstrumentationScopeInfo(), + timestamp: Date(), attributes: [:] ) } - func whenInvokingAddLogRecord(withLogRecord logRecord: LogRecord) { + func whenInvokingAddLogRecord(withLogRecord logRecord: ReadableLogRecord) { sut.addLogRecord(logRecord: logRecord) } diff --git a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift index 539295ec..6e304f9f 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift @@ -161,8 +161,8 @@ private extension StorageEmbraceLogExporterTests { XCTAssertEqual(batcher.addLogRecordInvocationCount, logCount) } - func thenRecordMatches(record: LogRecord, body: String, attributes: [String: PersistableValue]) { - XCTAssertEqual(record.body, body) + func thenRecordMatches(record: ReadableLogRecord, body: String, attributes: [String: AttributeValue]) { + XCTAssertEqual(record.body!.description, body) XCTAssertEqual(record.attributes, attributes) } @@ -189,9 +189,9 @@ private extension StorageEmbraceLogExporterTests { class SpyLogBatcher: LogBatcher { private(set) var didCallAddLogRecord: Bool = false private(set) var addLogRecordInvocationCount: Int = 0 - private(set) var logRecords = [LogRecord]() + private(set) var logRecords = [ReadableLogRecord]() - func addLogRecord(logRecord: LogRecord) { + func addLogRecord(logRecord: ReadableLogRecord) { didCallAddLogRecord = true addLogRecordInvocationCount += 1 logRecords.append(logRecord) diff --git a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift index 9dc24756..0bd767ae 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift @@ -10,6 +10,7 @@ import EmbraceUploadInternal import EmbraceCommonInternal import EmbraceConfigInternal import TestSupport +import OpenTelemetryApi class LogControllerTests: XCTestCase { private var sut: LogController! @@ -79,7 +80,7 @@ class LogControllerTests: XCTestCase { givenStorage(withLogs: [log]) givenLogController() whenInvokingSetup() - try thenFetchesResourcesFromStorage(processId: log.processIdentifier) + try thenFetchesResourcesFromStorage(processId: log.processId!) } func testHavingLogsWithNoSessionId_onSetup_fetchesMetadataFromStorage() throws { @@ -87,7 +88,7 @@ class LogControllerTests: XCTestCase { givenStorage(withLogs: [log]) givenLogController() whenInvokingSetup() - try thenFetchesMetadataFromStorage(processId: log.processIdentifier) + try thenFetchesMetadataFromStorage(processId: log.processId!) } func testHavingLogsForLessThanABatch_onSetup_logUploaderShouldSendASingleBatch() { @@ -297,8 +298,8 @@ private extension LogControllerTests { sessionController = .init() sessionController.currentSession = .init( id: .random, - state: .foreground, processId: .random, + state: .foreground, traceId: UUID().uuidString, spanId: UUID().uuidString, startTime: Date() @@ -311,7 +312,7 @@ private extension LogControllerTests { } func givenStorageThatThrowsException() { - storage = .init(SpyStorage(shouldThrow: true)) + storage = .init(SpyStorage()) } func whenInvokingSetup() { @@ -457,14 +458,14 @@ private extension LogControllerTests { func randomLogRecord(sessionId: SessionIdentifier? = nil) -> LogRecord { - var attributes: [String: PersistableValue] = [:] + var attributes: [String: AttributeValue] = [:] if let sessionId = sessionId { - attributes["session.id"] = PersistableValue(sessionId.toString) + attributes["session.id"] = AttributeValue(sessionId.toString) } return LogRecord( - identifier: .random, - processIdentifier: .random, + id: .random, + processId: .random, severity: .info, body: UUID().uuidString, attributes: attributes @@ -477,9 +478,3 @@ private extension LogControllerTests { } } } - -extension LogRecord: Equatable { - public static func == (lhs: LogRecord, rhs: LogRecord) -> Bool { - lhs.identifier == rhs.identifier - } -} diff --git a/Tests/EmbraceCoreTests/Internal/Logs/LogsBatchTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/LogsBatchTests.swift index 309b0eb2..ff197a57 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/LogsBatchTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/LogsBatchTests.swift @@ -106,12 +106,12 @@ private extension LogsBatchTests { func randomLog(date: Date = Date()) -> LogRecord { let recentLog = LogRecord( - identifier: .init(), - processIdentifier: .random, + id: .init(), + processId: .random, severity: .info, body: UUID().uuidString, - attributes: [:], - timestamp: date + timestamp: date, + attributes: [:] ) return recentLog } diff --git a/Tests/EmbraceCoreTests/Internal/ResourceStorageExporterTests.swift b/Tests/EmbraceCoreTests/Internal/ResourceStorageExporterTests.swift index f83052b6..d7b97b4e 100644 --- a/Tests/EmbraceCoreTests/Internal/ResourceStorageExporterTests.swift +++ b/Tests/EmbraceCoreTests/Internal/ResourceStorageExporterTests.swift @@ -14,20 +14,20 @@ final class ResourceStorageExporterTests: XCTestCase { let storage = try EmbraceStorage.createInMemoryDb() let exporter = ResourceStorageExporter(storage: storage) - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: "permanent", type: .resource, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: "session", type: .resource, lifespan: .session, lifespanId: "sessionId" ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: "process", type: .resource, diff --git a/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift b/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift index 9964bbe5..00af01ff 100644 --- a/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift +++ b/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift @@ -43,10 +43,10 @@ final class StorageSpanExporterTests: XCTestCase { hasEnded: false ) // When spans are exported - exporter.export(spans: [closedSpanData]) - exporter.export(spans: [updated_closedSpanData]) + _ = exporter.export(spans: [closedSpanData]) + _ = exporter.export(spans: [updated_closedSpanData]) - let exportedSpans: [SpanRecord] = try storage.fetchAll() + let exportedSpans: [SpanRecord] = storage.fetchAll() XCTAssertTrue(exportedSpans.count == 1) let exportedSpan = try XCTUnwrap(exportedSpans.first) @@ -87,10 +87,10 @@ final class StorageSpanExporterTests: XCTestCase { hasEnded: false ) // When spans are exported - exporter.export(spans: [openSpanData]) - exporter.export(spans: [updated_openSpanData]) + _ = exporter.export(spans: [openSpanData]) + _ = exporter.export(spans: [updated_openSpanData]) - let exportedSpans: [SpanRecord] = try storage.fetchAll() + let exportedSpans: [SpanRecord] = storage.fetchAll() XCTAssertTrue(exportedSpans.count == 1) let exportedSpan = exportedSpans.first diff --git a/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift b/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift index 053d5363..17892c2a 100644 --- a/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift @@ -7,12 +7,13 @@ import EmbraceStorageInternal import EmbraceCommonInternal import TestSupport @testable import EmbraceCore +import OpenTelemetryApi class LogPayloadBuilderTests: XCTestCase { func test_build_addsLogIdAttribute() throws { let logId = LogIdentifier(value: try XCTUnwrap(UUID(uuidString: "53B55EDD-889A-4876-86BA-6798288B609C"))) - let record = LogRecord(identifier: logId, - processIdentifier: .random, + let record = LogRecord(id: logId, + processId: .random, severity: .info, body: "Hello World", attributes: .empty()) @@ -25,14 +26,14 @@ class LogPayloadBuilderTests: XCTestCase { } func test_buildLogRecordWithAttributes_mapsKeyValuesAsAttributeStruct() { - let originalAttributes: [String: PersistableValue] = [ + let originalAttributes: [String: AttributeValue] = [ "string_attribute": .string("string"), "integer_attribute": .int(1), "boolean_attribute": .bool(false), "double_attribute": .double(5.0) ] - let record = LogRecord(identifier: .random, - processIdentifier: .random, + let record = LogRecord(id: .random, + processId: .random, severity: .info, body: .random(), attributes: originalAttributes) @@ -40,21 +41,20 @@ class LogPayloadBuilderTests: XCTestCase { let payload = LogPayloadBuilder.build(log: record) XCTAssertGreaterThanOrEqual(payload.attributes.count, originalAttributes.count) - originalAttributes.forEach { originalAttributeKey, originalAttributeValue in - let originalAttributeWasMigrated = payload.attributes.contains { attribute in - attribute.key == originalAttributeKey && attribute.value == originalAttributeValue.description - } - XCTAssertTrue(originalAttributeWasMigrated) + + for (key, value) in originalAttributes { + let attribute = payload.attributes.first(where: { $0.key == key && $0.value == value.description }) + XCTAssertNotNil(attribute) } } func test_manualBuild() throws { // given a session in storage let storage = try EmbraceStorage.createInMemoryDb() - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSince1970: 0), @@ -62,26 +62,26 @@ class LogPayloadBuilderTests: XCTestCase { ) // given metadata in storage of that session - try storage.addMetadata( + storage.addMetadata( key: AppResourceKey.appVersion.rawValue, value: "1.0.0", type: .requiredResource, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: UserResourceKey.name.rawValue, value: "test", type: .customProperty, lifespan: .session, lifespanId: TestConstants.sessionId.toString ) - try storage.addMetadata( + storage.addMetadata( key: "tag1", value: "tag1", type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "tag2", value: "tag2", type: .personaTag, diff --git a/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift b/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift index f4af7d9f..0c15bb99 100644 --- a/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift +++ b/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift @@ -12,9 +12,9 @@ final class PayloadUtilTests: XCTestCase { func test_fetchResources() throws { // given let mockResources: [MetadataRecord] = [ - .init( + MetadataRecord( key: "fake_res", - value: .string("fake_value"), + value: "fake_value", type: .requiredResource, lifespan: .process, lifespanId: ProcessIdentifier.current.hex @@ -68,7 +68,7 @@ final class PayloadUtilTests: XCTestCase { let mockResources: [MetadataRecord] = [ .init( key: "fake_res", - value: .string("fake_value"), + value: "fake_value", type: .customProperty, lifespan: .session, lifespanId: sessionId.toString diff --git a/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift b/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift index 1e705a35..1ddcafb5 100644 --- a/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift @@ -18,8 +18,8 @@ final class SessionPayloadBuilderTests: XCTestCase { sessionRecord = SessionRecord( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSince1970: 0), @@ -28,18 +28,13 @@ final class SessionPayloadBuilderTests: XCTestCase { } override func tearDownWithError() throws { - try storage.dbQueue.write { db in - try SessionRecord.deleteAll(db) - try MetadataRecord.deleteAll(db) - } - sessionRecord = nil - try storage.teardown() + storage.coreData.destroy() } func test_counterMissing() throws { // given no existing counter in storage - var resource = try storage.fetchMetadata( + var resource = storage.fetchMetadata( key: SessionPayloadBuilder.resourceName, type: .requiredResource, lifespan: .permanent @@ -50,25 +45,25 @@ final class SessionPayloadBuilderTests: XCTestCase { _ = SessionPayloadBuilder.build(for: sessionRecord, storage: storage) // then a resource is created with the correct value - resource = try storage.fetchMetadata( + resource = storage.fetchMetadata( key: SessionPayloadBuilder.resourceName, type: .requiredResource, lifespan: .permanent ) - XCTAssertEqual(resource!.value, .string("1")) + XCTAssertEqual(resource!.value, "1") } func test_existingCounter() throws { // given existing counter in storage - try storage.addMetadata( + storage.addMetadata( key: SessionPayloadBuilder.resourceName, value: "10", type: .requiredResource, lifespan: .permanent ) - var resource = try storage.fetchMetadata( + var resource = storage.fetchMetadata( key: SessionPayloadBuilder.resourceName, type: .requiredResource, lifespan: .permanent @@ -79,12 +74,12 @@ final class SessionPayloadBuilderTests: XCTestCase { _ = SessionPayloadBuilder.build(for: sessionRecord, storage: storage) // then the counter is updated correctly - resource = try storage.fetchMetadata( + resource = storage.fetchMetadata( key: SessionPayloadBuilder.resourceName, type: .requiredResource, lifespan: .permanent ) - XCTAssertEqual(resource!.value, .string("11")) + XCTAssertEqual(resource!.value, "11") } } diff --git a/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift b/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift index 0067709d..f594ccc7 100644 --- a/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift @@ -21,8 +21,8 @@ final class SpansPayloadBuilderTests: XCTestCase { sessionRecord = SessionRecord( id: TestConstants.sessionId, - state: .foreground, processId: .random, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSince1970: 50), @@ -31,14 +31,8 @@ final class SpansPayloadBuilderTests: XCTestCase { } override func tearDownWithError() throws { - try storage.dbQueue.write { db in - try SessionRecord.deleteAll(db) - try SpanRecord.deleteAll(db) - } - sessionRecord = nil - - try storage.teardown() + storage.coreData.destroy() } func testSpan(startTime: Date, endTime: Date?, name: String?) -> SpanData { @@ -66,7 +60,7 @@ final class SpansPayloadBuilderTests: XCTestCase { let spanData = testSpan(startTime: startTime, endTime: endTime, name: name) let data = try spanData.toJSON() - let record = SpanRecord( + storage.upsertSpan( id: id ?? spanData.spanId.hexString, name: spanData.name, traceId: traceId ?? spanData.traceId.hexString, @@ -76,8 +70,6 @@ final class SpansPayloadBuilderTests: XCTestCase { endTime: spanData.hasEnded ? spanData.endTime : nil ) - try storage.upsertSpan(record) - return spanData } @@ -85,8 +77,8 @@ final class SpansPayloadBuilderTests: XCTestCase { // given no session span and a session record with nil end time let record = SessionRecord( id: TestConstants.sessionId, - state: .foreground, processId: .random, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSince1970: 50), diff --git a/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift b/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift index e2ab8b65..0b865b5d 100644 --- a/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift +++ b/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift @@ -222,19 +222,16 @@ final class EmbraceCoreTests: XCTestCase { embrace.add(event: Breadcrumb(message: "Test Breadcrumb", attributes: [:])) // Check the event was flushed to storage immediately after. - try storage.dbQueue.inDatabase { db in - let records = try SpanRecord.fetchAll(db) - if let sessionSpan = records.first(where: { $0.name == "emb-session" }) { - let spanData = try JSONDecoder().decode(SpanData.self, from: sessionSpan.data) - let breadcrumbEvent = spanData.events.first(where: { - $0.name == "emb-breadcrumb" && - $0.attributes["message"] == .string("Test Breadcrumb") - }) - XCTAssertNotNil(breadcrumbEvent) - } else { - XCTFail("\(#function): Failed, no session span found on storage.") - } - } + let spans: [SpanRecord] = embrace.storage.fetchAll() + let sessionSpan = spans.first(where: { $0.name == "emb-session" }) + XCTAssertNotNil(sessionSpan) + + let spanData = try JSONDecoder().decode(SpanData.self, from: sessionSpan!.data) + let breadcrumbEvent = spanData.events.first(where: { + $0.name == "emb-breadcrumb" && + $0.attributes["message"] == .string("Test Breadcrumb") + }) + XCTAssertNotNil(breadcrumbEvent) } func test_ManualSpanExport() throws { @@ -251,15 +248,12 @@ final class EmbraceCoreTests: XCTestCase { embrace.flush(span) - try storage.dbQueue.inDatabase { db in - let records = try SpanRecord.fetchAll(db) - if let sessionSpan = records.first(where: { $0.name == "test_manual_export_span" }) { - let spanData = try JSONDecoder().decode(SpanData.self, from: sessionSpan.data) - XCTAssertFalse(spanData.hasEnded) - } else { - XCTFail("\(#function): Failed, span not found in storage.") - } - } + let spans: [SpanRecord] = embrace.storage.fetchAll() + let record = spans.first(where: { $0.name == "test_manual_export_span" }) + XCTAssertNotNil(record) + + let spanData = try JSONDecoder().decode(SpanData.self, from: record!.data) + XCTAssertFalse(spanData.hasEnded) } // MARK: - Crash+CrashRecorder tests diff --git a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandler+PersonaTagTests.swift b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandler+PersonaTagTests.swift index a5379633..76e97464 100644 --- a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandler+PersonaTagTests.swift +++ b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandler+PersonaTagTests.swift @@ -18,12 +18,12 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { override func setUpWithError() throws { storage = try EmbraceStorage.createInMemoryDb() sessionController = MockSessionController() + sessionController.storage = storage sessionController.startSession(state: .foreground) - try storage.upsertSession(sessionController.currentSession!) } override func tearDownWithError() throws { - try storage.teardown() + storage.coreData.destroy() sessionController = nil } @@ -55,7 +55,7 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { // given limits reached on persona tags for i in 1...storage.options.personaTagsLimit { - try storage.addMetadata(key: "test\(i)", value: "test\(i)", type: .personaTag, lifespan: .permanent) + storage.addMetadata(key: "test\(i)", value: "test\(i)", type: .personaTag, lifespan: .permanent) } // when adding a persona tag @@ -80,25 +80,25 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { let handler = MetadataHandler(storage: storage, sessionController: sessionController) // given some persona tags in storage - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .session, - lifespanId: sessionController.currentSession!.id.toString + lifespanId: sessionController.currentSession!.idRaw ) // when fetching the current persona tags @@ -114,25 +114,25 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { let handler = MetadataHandler(storage: storage, sessionController: sessionController) // given some persona tags in storage - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: "permanent", type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: "process", type: .personaTag, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: "session", type: .personaTag, lifespan: .session, - lifespanId: sessionController.currentSession!.id.toString + lifespanId: sessionController.currentSession!.idRaw ) // when fetching the current persona tags @@ -148,25 +148,25 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { let handler = MetadataHandler(storage: storage, sessionController: sessionController) // given some persona tags in storage - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: "permanent", type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: "process", type: .personaTag, lifespan: .process, lifespanId: ProcessIdentifier.random.hex ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: "session", type: .personaTag, lifespan: .session, - lifespanId: sessionController.currentSession!.id.toString + lifespanId: sessionController.currentSession!.idRaw ) // when fetching the current persona tags @@ -182,28 +182,28 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { let handler = MetadataHandler(storage: storage, sessionController: sessionController) // given some persona tags in storage - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .session, - lifespanId: sessionController.currentSession!.id.toString + lifespanId: sessionController.currentSession!.idRaw ) - try storage.removeMetadata(key: "permanent", type: .personaTag, lifespan: .permanent, lifespanId: "") + storage.removeMetadata(key: "permanent", type: .personaTag, lifespan: .permanent, lifespanId: "") // when fetching the current persona tags let tags = handler.currentPersonas @@ -220,32 +220,32 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { let handler = MetadataHandler(storage: storage, sessionController: sessionController) // given some persona tags in storage - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .session, - lifespanId: sessionController.currentSession!.id.toString + lifespanId: sessionController.currentSession!.idRaw ) // when removing a persona tag try handler.remove(persona: "session", lifespan: .session) // then the persona tag is removed - let tags = try storage.fetchPersonaTagsForSessionId(sessionController.currentSession!.id) + let tags = storage.fetchPersonaTagsForSessionId(sessionController.currentSession!.id!) XCTAssertEqual(tags.count, 2) XCTAssertEqual(Set(tags.map(\.key)), Set(["permanent", "process"])) } @@ -255,32 +255,32 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { let handler = MetadataHandler(storage: storage, sessionController: sessionController) // given some persona tags in storage - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .session, - lifespanId: sessionController.currentSession!.id.toString + lifespanId: sessionController.currentSession!.idRaw ) // when removing a persona tag try handler.remove(persona: "permanent", lifespan: .session) // then the persona tag is removed - let tags = try storage.fetchPersonaTagsForSessionId(sessionController.currentSession!.id) + let tags = storage.fetchPersonaTagsForSessionId(sessionController.currentSession!.id!) XCTAssertEqual(tags.count, 3) XCTAssertEqual(Set(tags.map(\.key)), Set(["permanent", "process", "session"])) } @@ -291,32 +291,32 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { let handler = MetadataHandler(storage: storage, sessionController: sessionController) // given some persona tags in storage - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .session, - lifespanId: sessionController.currentSession!.id.toString + lifespanId: sessionController.currentSession!.idRaw ) // when removing all persona tags try handler.removeAllPersonas() // then the persona tags are removed - let tags = try storage.fetchPersonaTagsForSessionId(sessionController.currentSession!.id) + let tags = storage.fetchPersonaTagsForSessionId(sessionController.currentSession!.id!) XCTAssertEqual(tags.count, 0) } @@ -325,34 +325,33 @@ final class MetadataHandler_PersonaTagTests: XCTestCase { let handler = MetadataHandler(storage: storage, sessionController: sessionController) // given some persona tags in storage - try storage.addMetadata( + storage.addMetadata( key: "permanent", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .permanent ) - try storage.addMetadata( + storage.addMetadata( key: "process", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - try storage.addMetadata( + storage.addMetadata( key: "session", value: PersonaTag.metadataValue, type: .personaTag, lifespan: .session, - lifespanId: sessionController.currentSession!.id.toString + lifespanId: sessionController.currentSession!.idRaw ) // when removing all persona tags try handler.removeAllPersonas(lifespans: [.permanent]) // then the persona tags are removed - let tags = try storage.fetchPersonaTagsForSessionId(sessionController.currentSession!.id) + let tags = storage.fetchPersonaTagsForSessionId(sessionController.currentSession!.id!) XCTAssertEqual(tags.count, 2) XCTAssertEqual(Set(tags.map(\.key)), Set(["process", "session"])) } - } diff --git a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandler+UserTests.swift b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandler+UserTests.swift index 0b0ad642..08ceb03a 100644 --- a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandler+UserTests.swift +++ b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandler+UserTests.swift @@ -19,7 +19,7 @@ final class MetadataHandler_UserTests: XCTestCase { } override func tearDownWithError() throws { - try storage.teardown() + storage.coreData.destroy() sessionController = nil } diff --git a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift index 4b6e3dc8..93d102b4 100644 --- a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift @@ -21,10 +21,10 @@ final class MetadataHandlerTests: XCTestCase { sessionController = MockSessionController() sessionController.startSession(state: .foreground) - try storage.addSession( - id: sessionController.currentSession!.id, - state: .foreground, + storage.addSession( + id: sessionController.currentSession!.id!, processId: .current, + state: .foreground, traceId: .random(), spanId: .random(), startTime: Date() @@ -32,7 +32,7 @@ final class MetadataHandlerTests: XCTestCase { } override func tearDownWithError() throws { - try storage.teardown() + storage.coreData.destroy() sessionController = nil } @@ -84,20 +84,14 @@ final class MetadataHandlerTests: XCTestCase { } // when adding metadata with invalid values - let expectation = XCTestExpectation() try handler.addResource(key: "test", value: invalidValue, lifespan: .permanent) try handler.addProperty(key: "test", value: invalidValue, lifespan: .permanent) // then the values are truncated - try storage.dbQueue.read { db in - let records = try MetadataRecord.fetchAll(db) - for metadata in records { - XCTAssertEqual(metadata.stringValue!.count, MetadataHandler.maxValueLength) - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let metadata: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(metadata.count, 2) + XCTAssertEqual(metadata[0].value.count, MetadataHandler.maxValueLength) + XCTAssertEqual(metadata[1].value.count, MetadataHandler.maxValueLength) } func test_currentSession_validation() throws { @@ -142,11 +136,11 @@ final class MetadataHandlerTests: XCTestCase { // given limits reached on metadata for i in 1...storage.options.resourcesLimit { - try storage.addMetadata(key: "resource\(i)", value: "test", type: .resource, lifespan: .permanent) + storage.addMetadata(key: "resource\(i)", value: "test", type: .resource, lifespan: .permanent) } for i in 1...storage.options.customPropertiesLimit { - try storage.addMetadata(key: "resource\(i)", value: "test", type: .customProperty, lifespan: .permanent) + storage.addMetadata(key: "resource\(i)", value: "test", type: .customProperty, lifespan: .permanent) } // when adding a resource @@ -186,7 +180,7 @@ final class MetadataHandlerTests: XCTestCase { // when added try handler.addProperty(key: "foo", value: "bar", lifespan: .session) - let firstFetch = try storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id) + let firstFetch = storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id!) let item = firstFetch.first { record in record.key == "foo" } @@ -195,7 +189,7 @@ final class MetadataHandlerTests: XCTestCase { // When removed try handler.removeProperty(key: "foo", lifespan: .session) - let secondFetch = try storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id) + let secondFetch = storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id!) let result = secondFetch.first { record in record.key == "foo" } @@ -205,23 +199,23 @@ final class MetadataHandlerTests: XCTestCase { func test_remove_doesNot_removeMetadataWithSessionLifespan_whenSessionChanges() throws { let handler = MetadataHandler(storage: storage, sessionController: sessionController) - let firstSessionId = sessionController.currentSession!.id + let firstSessionId = sessionController.currentSession!.id! // when added to first session try handler.addProperty(key: "foo", value: "bar", lifespan: .session) // start new session let newSession = sessionController.startSession(state: .foreground) - let secondSessionId = newSession!.id - try storage.addSession( + let secondSessionId = newSession!.id! + storage.addSession( id: secondSessionId, - state: .foreground, processId: .current, + state: .foreground, traceId: .random(), spanId: .random(), startTime: Date() ) - let fetch1 = try storage.fetchCustomPropertiesForSessionId(firstSessionId) + let fetch1 = storage.fetchCustomPropertiesForSessionId(firstSessionId) let result1 = fetch1.first { record in record.key == "foo" } @@ -230,13 +224,13 @@ final class MetadataHandlerTests: XCTestCase { // When removed try handler.removeProperty(key: "foo", lifespan: .session) - let fetch2 = try storage.fetchCustomPropertiesForSessionId(secondSessionId) + let fetch2 = storage.fetchCustomPropertiesForSessionId(secondSessionId) let result2 = fetch2.first { record in record.key == "foo" } XCTAssertNil(result2) // not present from second session - let fetch3 = try storage.fetchCustomPropertiesForSessionId(firstSessionId) + let fetch3 = storage.fetchCustomPropertiesForSessionId(firstSessionId) let result3 = fetch3.first { record in record.key == "foo" } @@ -249,7 +243,7 @@ final class MetadataHandlerTests: XCTestCase { // when added try handler.addProperty(key: "foo", value: "bar", lifespan: .process) - let firstFetch = try storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id) + let firstFetch = storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id!) let item = firstFetch.first { record in record.key == "foo" } @@ -258,7 +252,7 @@ final class MetadataHandlerTests: XCTestCase { // When removed try handler.removeProperty(key: "foo", lifespan: .process) - let secondFetch = try storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id) + let secondFetch = storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id!) let result = secondFetch.first { record in record.key == "foo" } @@ -270,17 +264,17 @@ final class MetadataHandlerTests: XCTestCase { let otherProcessId = ProcessIdentifier.random let otherSessionId = SessionIdentifier.random - try storage.addSession( + storage.addSession( id: otherSessionId, - state: .foreground, processId: otherProcessId, + state: .foreground, traceId: .random(), spanId: .random(), startTime: Date() ) // when added to process that occurred "before" - try storage.addMetadata( + storage.addMetadata( key: "foo", value: "bar", type: .customProperty, @@ -292,14 +286,14 @@ final class MetadataHandlerTests: XCTestCase { try handler.removeProperty(key: "foo", lifespan: .process) // exists in other session - let fetch1 = try storage.fetchCustomPropertiesForSessionId(otherSessionId) + let fetch1 = storage.fetchCustomPropertiesForSessionId(otherSessionId) let result1 = fetch1.first { record in record.key == "foo" } XCTAssertNotNil(result1) // does not exist in current session - let fetch2 = try storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id) + let fetch2 = storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id!) let result2 = fetch2.first { record in record.key == "foo" } @@ -312,7 +306,7 @@ final class MetadataHandlerTests: XCTestCase { // when added try handler.addProperty(key: "foo", value: "bar", lifespan: .permanent) - let firstFetch = try storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id) + let firstFetch = storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id!) let item = firstFetch.first { record in record.key == "foo" } @@ -321,7 +315,7 @@ final class MetadataHandlerTests: XCTestCase { // When removed try handler.removeProperty(key: "foo", lifespan: .permanent) - let secondFetch = try storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id) + let secondFetch = storage.fetchCustomPropertiesForSessionId(sessionController.currentSession!.id!) let result = secondFetch.first { record in record.key == "foo" } @@ -332,8 +326,8 @@ final class MetadataHandlerTests: XCTestCase { func test_coreDataClone() throws { // given stored metadata for i in 1...3 { - try storage.addMetadata(key: "resource\(i)", value: "test", type: .resource, lifespan: .permanent) - try storage.addMetadata(key: "property\(i)", value: "test", type: .customProperty, lifespan: .permanent) + storage.addMetadata(key: "resource\(i)", value: "test", type: .resource, lifespan: .permanent) + storage.addMetadata(key: "property\(i)", value: "test", type: .customProperty, lifespan: .permanent) } // when initializing a metadata handler diff --git a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift index dbe0c027..381326c4 100644 --- a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift +++ b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift @@ -57,7 +57,7 @@ final class SessionControllerTests: XCTestCase { } override func tearDownWithError() throws { - try storage.teardown() + storage.coreData.destroy() upload = nil controller = nil } @@ -115,7 +115,7 @@ final class SessionControllerTests: XCTestCase { func test_startSession_saves_foregroundSession() throws { let session = controller.startSession(state: .foreground) - let sessions: [SessionRecord] = try storage.fetchAll() + let sessions: [SessionRecord] = storage.fetchAll() XCTAssertEqual(sessions.count, 1) XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "foreground") @@ -160,7 +160,7 @@ final class SessionControllerTests: XCTestCase { let endTime = controller.endSession() - let sessions: [SessionRecord] = try storage.fetchAll() + let sessions: [SessionRecord] = storage.fetchAll() XCTAssertEqual(sessions.count, 1) XCTAssertEqual(sessions.first!.endTime!.timeIntervalSince1970, endTime.timeIntervalSince1970, accuracy: 0.1) @@ -176,7 +176,7 @@ final class SessionControllerTests: XCTestCase { let endTime = controller.endSession() - let sessions: [SessionRecord] = try storage.fetchAll() + let sessions: [SessionRecord] = storage.fetchAll() XCTAssertEqual(sessions.count, 1) XCTAssertEqual(sessions.first!.id, session!.id) XCTAssertEqual(sessions.first!.state, "foreground") @@ -212,11 +212,11 @@ final class SessionControllerTests: XCTestCase { XCTAssertEqual(EmbraceHTTPMock.requestsForUrl(testSessionsUrl()).count, 1) // then the session is no longer on storage - let session = try storage.fetchSession(id: TestConstants.sessionId) + let session = storage.fetchSession(id: TestConstants.sessionId) XCTAssertNil(session) // then the session upload data is no longer cached - let uploadData = try upload.cache.fetchAllUploadData() + let uploadData = upload.cache.fetchAllUploadData() XCTAssertEqual(uploadData.count, 0) } @@ -240,11 +240,11 @@ final class SessionControllerTests: XCTestCase { XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 1) // then the session is no longer on storage - let session = try storage.fetchSession(id: TestConstants.sessionId) + let session = storage.fetchSession(id: TestConstants.sessionId) XCTAssertNil(session) // then the session upload data cached - let uploadData = try upload.cache.fetchAllUploadData() + let uploadData = upload.cache.fetchAllUploadData() XCTAssertEqual(uploadData.count, 1) } @@ -259,7 +259,7 @@ final class SessionControllerTests: XCTestCase { } func test_update_assignsAppTerminated_toFalse_whenPresent() throws { - var session = controller.startSession(state: .foreground) + let session = controller.startSession(state: .foreground) session!.appTerminated = true controller.update(appTerminated: false) @@ -267,7 +267,7 @@ final class SessionControllerTests: XCTestCase { } func test_update_assignsAppTerminated_toTrue_whenPresent() throws { - var session = controller.startSession(state: .foreground) + let session = controller.startSession(state: .foreground) session!.appTerminated = false controller.update(appTerminated: true) @@ -275,12 +275,12 @@ final class SessionControllerTests: XCTestCase { } func test_update_changesTo_appTerminated_saveInStorage() throws { - var session = controller.startSession(state: .foreground) + let session = controller.startSession(state: .foreground) session!.appTerminated = false controller.update(appTerminated: true) - let sessions: [SessionRecord] = try storage.fetchAll() + let sessions: [SessionRecord] = storage.fetchAll() XCTAssertEqual(sessions.count, 1) XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "foreground") @@ -288,12 +288,12 @@ final class SessionControllerTests: XCTestCase { } func test_update_changesTo_sessionState_saveInStorage() throws { - var session = controller.startSession(state: .foreground) + let session = controller.startSession(state: .foreground) session!.appTerminated = false controller.update(state: .background) - let sessions: [SessionRecord] = try storage.fetchAll() + let sessions: [SessionRecord] = storage.fetchAll() XCTAssertEqual(sessions.count, 1) XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "background") @@ -334,7 +334,7 @@ final class SessionControllerTests: XCTestCase { controller.endSession() // then the session is stored - let sessions: [SessionRecord] = try storage.fetchAll() + let sessions: [SessionRecord] = storage.fetchAll() XCTAssertEqual(sessions.count, 1) XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "background") @@ -383,7 +383,7 @@ final class SessionControllerTests: XCTestCase { controller.endSession() // then the session is not stored - let sessions: [SessionRecord] = try storage.fetchAll() + let sessions: [SessionRecord] = storage.fetchAll() XCTAssertEqual(sessions.count, 0) } diff --git a/Tests/EmbraceCoreTests/Session/SessionSpanUtilsTests.swift b/Tests/EmbraceCoreTests/Session/SessionSpanUtilsTests.swift index 91a32f27..cddde796 100644 --- a/Tests/EmbraceCoreTests/Session/SessionSpanUtilsTests.swift +++ b/Tests/EmbraceCoreTests/Session/SessionSpanUtilsTests.swift @@ -128,8 +128,8 @@ final class SessionSpanUtilsTests: XCTestCase { let session = SessionRecord( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: TestConstants.date, @@ -205,8 +205,8 @@ final class SessionSpanUtilsTests: XCTestCase { // test ok status var session = SessionRecord( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: TestConstants.date, @@ -224,8 +224,8 @@ final class SessionSpanUtilsTests: XCTestCase { // test error status session = SessionRecord( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: TestConstants.date, @@ -311,8 +311,8 @@ private extension SessionSpanUtilsTests { return SessionRecord( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: TestConstants.date, @@ -325,6 +325,12 @@ private extension SessionSpanUtilsTests { } func givenCustomProperty(withKey key: String, value: String, lifespan: MetadataRecordLifespan) -> MetadataRecord { - .init(key: key, value: .string(value), type: .customProperty, lifespan: lifespan, lifespanId: .random()) + MetadataRecord( + key: key, + value: value, + type: .customProperty, + lifespan: lifespan, + lifespanId: .random() + ) } } diff --git a/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift b/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift index 89814d48..35903f27 100644 --- a/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift @@ -9,7 +9,6 @@ import EmbraceCommonInternal @testable import EmbraceStorageInternal @testable import EmbraceUploadInternal import TestSupport -import GRDB class UnsentDataHandlerTests: XCTestCase { let logger = MockLogger() @@ -65,17 +64,17 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) let otel = MockEmbraceOpenTelemetry() // given a finished session in the storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60), @@ -90,11 +89,11 @@ class UnsentDataHandlerTests: XCTestCase { XCTAssertEqual(EmbraceHTTPMock.requestsForUrl(testSpansUrl()).count, 1) // then the session is no longer on storage - let session = try storage.fetchSession(id: TestConstants.sessionId) + let session = storage.fetchSession(id: TestConstants.sessionId) XCTAssertNil(session) // then the session upload data is no longer cached - let uploadData = try upload.cache.fetchAllUploadData() + let uploadData = upload.cache.fetchAllUploadData() XCTAssertEqual(uploadData.count, 0) // then no log was sent @@ -107,17 +106,17 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) let otel = MockEmbraceOpenTelemetry() // given a finished session in the storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60), @@ -135,11 +134,11 @@ class UnsentDataHandlerTests: XCTestCase { XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 1) // then the session is no longer on storage - let session = try storage.fetchSession(id: TestConstants.sessionId) + let session = storage.fetchSession(id: TestConstants.sessionId) XCTAssertNil(session) // then the session upload data cached - let uploadData = try upload.cache.fetchAllUploadData() + let uploadData = upload.cache.fetchAllUploadData() XCTAssertEqual(uploadData.count, 1) // then no log was sent @@ -153,7 +152,7 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) @@ -164,10 +163,10 @@ class UnsentDataHandlerTests: XCTestCase { let report = crashReporter.mockReports[0] // given a finished session in the storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60), @@ -178,19 +177,15 @@ class UnsentDataHandlerTests: XCTestCase { UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, crashReporter: crashReporter) // then the crash report id is set on the session + let listener = CoreDataListener() let expectation1 = XCTestExpectation() - let observation = ValueObservation.tracking(SessionRecord.fetchAll) - let cancellable = observation.start(in: storage.dbQueue) { error in - XCTAssert(false, error.localizedDescription) - } onChange: { records in - if let record = records.first { - if record.crashReportId != nil { - expectation1.fulfill() - } + listener.onUpdatedObjects = { records in + if let record = records.first as? SessionRecord, + record.crashReportId != nil { + expectation1.fulfill() } } wait(for: [expectation1], timeout: .veryLongTimeout) - cancellable.cancel() // then a crash report was sent // then a session request was sent @@ -203,11 +198,11 @@ class UnsentDataHandlerTests: XCTestCase { XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 2) // then the session is no longer on storage - let session = try storage.fetchSession(id: TestConstants.sessionId) + let session = storage.fetchSession(id: TestConstants.sessionId) XCTAssertNil(session) // then the session and crash report upload data is no longer cached - let uploadData = try upload.cache.fetchAllUploadData() + let uploadData = upload.cache.fetchAllUploadData() XCTAssertEqual(uploadData.count, 0) // then the crash is not longer stored @@ -223,9 +218,6 @@ class UnsentDataHandlerTests: XCTestCase { XCTAssertEqual(otel.logs.count, 1) XCTAssertEqual(otel.logs[0].attributes["emb.type"], .string(LogType.crash.rawValue)) XCTAssertEqual(otel.logs[0].timestamp, report.timestamp) - - // clean up - cancellable.cancel() } func test_withCrashReporter_error() throws { @@ -234,7 +226,7 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) @@ -245,23 +237,20 @@ class UnsentDataHandlerTests: XCTestCase { let report = crashReporter.mockReports[0] // then the crash report id is set on the session + let listener = CoreDataListener() let didSendCrashesExpectation = XCTestExpectation() - let observation = ValueObservation.tracking(SessionRecord.fetchAll) - let cancellable = observation.start(in: storage.dbQueue) { error in - XCTAssert(false, error.localizedDescription) - } onChange: { records in - if let record = records.first { - if record.crashReportId != nil { - didSendCrashesExpectation.fulfill() - } + listener.onUpdatedObjects = { records in + if let record = records.first as? SessionRecord, + record.crashReportId != nil { + didSendCrashesExpectation.fulfill() } } // given a finished session in the storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60), @@ -272,7 +261,6 @@ class UnsentDataHandlerTests: XCTestCase { UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, crashReporter: crashReporter) wait(for: [didSendCrashesExpectation], timeout: .veryLongTimeout) - cancellable.cancel() // then a crash report request was attempted // then a session request was attempted @@ -285,11 +273,11 @@ class UnsentDataHandlerTests: XCTestCase { XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 2) // then the session is no longer on storage - let session = try storage.fetchSession(id: TestConstants.sessionId) + let session = storage.fetchSession(id: TestConstants.sessionId) XCTAssertNil(session) // then the session and crash report upload data are still cached - let uploadData = try upload.cache.fetchAllUploadData() + let uploadData = upload.cache.fetchAllUploadData() XCTAssertEqual(uploadData.count, 2) // then the crash is not longer stored @@ -314,7 +302,7 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) @@ -325,25 +313,23 @@ class UnsentDataHandlerTests: XCTestCase { let report = crashReporter.mockReports[0] // given an unfinished session in the storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60) ) // the crash report id and timestamp is set on the session + let listener = CoreDataListener() let expectation1 = XCTestExpectation() - let observation = ValueObservation.tracking(SessionRecord.fetchAll).print() - let cancellable = observation.start(in: storage.dbQueue) { error in - XCTAssert(false, error.localizedDescription) - } onChange: { records in - if let record = records.first { - if record.crashReportId != nil && record.endTime != nil { - expectation1.fulfill() - } + listener.onUpdatedObjects = { records in + if let record = records.first as? SessionRecord, + record.crashReportId != nil, + record.endTime != nil { + expectation1.fulfill() } } @@ -351,7 +337,6 @@ class UnsentDataHandlerTests: XCTestCase { UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, crashReporter: crashReporter) wait(for: [expectation1], timeout: 5000) - cancellable.cancel() // then a crash report was sent // then a session request was sent @@ -364,12 +349,12 @@ class UnsentDataHandlerTests: XCTestCase { XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 2) // then the session is no longer on storage - let session = try storage.fetchSession(id: TestConstants.sessionId) + let session = storage.fetchSession(id: TestConstants.sessionId) XCTAssertNil(session) // then the session and crash report upload data is no longer cached wait(timeout: .veryLongTimeout) { - try upload.cache.fetchAllUploadData().count == 0 + upload.cache.fetchAllUploadData().count == 0 } let expectation = XCTestExpectation() @@ -384,9 +369,6 @@ class UnsentDataHandlerTests: XCTestCase { XCTAssertEqual(otel.logs.count, 1) XCTAssertEqual(otel.logs[0].attributes["emb.type"], .string(LogType.crash.rawValue)) XCTAssertEqual(otel.logs[0].timestamp, report.timestamp) - - // clean up - cancellable.cancel() } func test_sendCrashLog() throws { @@ -395,7 +377,7 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) let otel = MockEmbraceOpenTelemetry() @@ -405,10 +387,10 @@ class UnsentDataHandlerTests: XCTestCase { let report = crashReporter.mockReports[0] // given a finished session in the storage - let session = try storage.addSession( + let session = storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60), @@ -434,7 +416,7 @@ class UnsentDataHandlerTests: XCTestCase { XCTAssertEqual(EmbraceHTTPMock.totalRequestCount(), 1) // then the crash log upload data is no longer cached - let uploadData = try upload.cache.fetchAllUploadData() + let uploadData = upload.cache.fetchAllUploadData() XCTAssertEqual(uploadData.count, 0) // then the raw crash log was constructed correctly @@ -457,24 +439,24 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) let otel = MockEmbraceOpenTelemetry() // given an unfinished session in the storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60) ) // given old closed span in storage - let oldSpan = try storage.addSpan( + storage.upsertSpan( id: "oldSpan", name: "test", traceId: "traceId", @@ -485,7 +467,7 @@ class UnsentDataHandlerTests: XCTestCase { ) // given open span in storage - _ = try storage.addSpan( + storage.upsertSpan( id: TestConstants.spanId, name: "test", traceId: TestConstants.traceId, @@ -501,32 +483,19 @@ class UnsentDataHandlerTests: XCTestCase { // then the old closed span was removed // and the open span was closed - let expectation1 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertFalse(try oldSpan.exists(db)) - - let span = try SpanRecord.fetchOne(db) - XCTAssertEqual(span!.id, TestConstants.spanId) - XCTAssertEqual(span!.traceId, TestConstants.traceId) - XCTAssertNotNil(span!.endTime) - - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: .defaultTimeout) + var spans: [SpanRecord] = storage.fetchAll() + XCTAssertEqual(spans.count, 1) + XCTAssertEqual(spans[0].id, TestConstants.spanId) + XCTAssertEqual(spans[0].traceId, TestConstants.traceId) + XCTAssertNotNil(spans[0].endTime) // when sending unsent sessions again UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel) // then the span that was closed for the last session // is not valid anymore, and therefore removed - let expectation2 = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try SpanRecord.fetchCount(db), 0) - expectation2.fulfill() - } - - wait(for: [expectation2], timeout: .defaultTimeout) + spans = storage.fetchAll() + XCTAssertEqual(spans.count, 0) } func test_metadataCleanUp_sendUnsendData() throws { @@ -536,52 +505,52 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) let otel = MockEmbraceOpenTelemetry() // given an unfinished session in the storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60) ) // given metadata in storage - let permanentMetadata = try storage.addMetadata( - key: "test", + storage.addMetadata( + key: "permanent", value: "test", type: .requiredResource, lifespan: .permanent ) - let sameSessionId = try storage.addMetadata( - key: "test", + storage.addMetadata( + key: "sameSessionId", value: "test", type: .requiredResource, lifespan: .session, lifespanId: TestConstants.sessionId.toString ) - let sameProcessId = try storage.addMetadata( - key: "test", + storage.addMetadata( + key: "sameProcessId", value: "test", type: .requiredResource, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - let differentSessionId = try storage.addMetadata( - key: "test", + storage.addMetadata( + key: "differentSessionId", value: "test", type: .requiredResource, lifespan: .session, lifespanId: "test" ) - let differentProcessId = try storage.addMetadata( - key: "test", + storage.addMetadata( + key: "differentProcessId", value: "test", type: .requiredResource, lifespan: .process, @@ -597,17 +566,12 @@ class UnsentDataHandlerTests: XCTestCase { ) // then all metadata is cleaned up - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try permanentMetadata!.exists(db)) - XCTAssert(try sameSessionId!.exists(db)) - XCTAssert(try sameProcessId!.exists(db)) - XCTAssertFalse(try differentSessionId!.exists(db)) - XCTAssertFalse(try differentProcessId!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertNotNil(records.first(where: { $0.key == "permanent"})) + XCTAssertNotNil(records.first(where: { $0.key == "sameSessionId"})) + XCTAssertNotNil(records.first(where: { $0.key == "sameProcessId"})) + XCTAssertNil(records.first(where: { $0.key == "differentSessionId"})) + XCTAssertNil(records.first(where: { $0.key == "differentProcessId"})) } func test_spanCleanUp_uploadSession() throws { @@ -617,22 +581,22 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) // given an unfinished session in the storage - let session = try storage.addSession( + let session = storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60) ) // given old closed span in storage - let oldSpan = try storage.addSpan( + storage.upsertSpan( id: "oldSpan", name: "test", traceId: "traceId", @@ -648,18 +612,10 @@ class UnsentDataHandlerTests: XCTestCase { // then the old closed span was removed // and the session was removed - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertFalse(try oldSpan.exists(db)) - XCTAssertEqual(try SpanRecord.fetchCount(db), 0) - - XCTAssertFalse(try session.exists(db)) - XCTAssertEqual(try SessionRecord.fetchCount(db), 0) - - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let spans: [SpanRecord] = storage.fetchAll() + let sessions: [SessionRecord] = storage.fetchAll() + XCTAssertEqual(spans.count, 0) + XCTAssertEqual(sessions.count, 0) } func test_metadataCleanUp_uploadSession() throws { @@ -669,36 +625,36 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) // given an unfinished session in the storage - let session = try storage.addSession( + let session = storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: ProcessIdentifier.current, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date(timeIntervalSinceNow: -60) ) // given metadata in storage - let permanentMetadata = try storage.addMetadata( - key: "test", + storage.addMetadata( + key: "permanent", value: "test", type: .requiredResource, lifespan: .permanent ) - let sameProcessId = try storage.addMetadata( - key: "test", + storage.addMetadata( + key: "sameProcessId", value: "test", type: .requiredResource, lifespan: .process, lifespanId: ProcessIdentifier.current.hex ) - let differentProcessId = try storage.addMetadata( - key: "test", + storage.addMetadata( + key: "differentProcessId", value: "test", type: .requiredResource, lifespan: .process, @@ -710,15 +666,10 @@ class UnsentDataHandlerTests: XCTestCase { wait(delay: .longTimeout) // then metadata is correctly cleaned up - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try permanentMetadata!.exists(db)) - XCTAssert(try sameProcessId!.exists(db)) - XCTAssertFalse(try differentProcessId!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertNotNil(records.first(where: { $0.key == "permanent"})) + XCTAssertNotNil(records.first(where: { $0.key == "sameProcessId"})) + XCTAssertNil(records.first(where: { $0.key == "differentProcessId"})) } func test_logsUpload() throws { @@ -728,7 +679,7 @@ class UnsentDataHandlerTests: XCTestCase { // given a storage and upload modules let storage = try EmbraceStorage.createInMemoryDb() - defer { try? storage.teardown() } + defer { storage.coreData.destroy() } let upload = try EmbraceUpload(options: uploadOptions, logger: logger, queue: queue, semaphore: .init(value: .max)) let logController = LogController( @@ -741,13 +692,13 @@ class UnsentDataHandlerTests: XCTestCase { // given logs in storage for _ in 0...5 { - try storage.writeLog(LogRecord( - identifier: LogIdentifier.random, - processIdentifier: TestConstants.processId, + storage.createLog( + id: LogIdentifier.random, + processId: TestConstants.processId, severity: .debug, body: "test", attributes: [:] - )) + ) } // when sending unsent data diff --git a/Tests/EmbraceCoreTests/TestSupport/Records/LogRecord+Init.swift b/Tests/EmbraceCoreTests/TestSupport/Records/LogRecord+Init.swift new file mode 100644 index 00000000..ea213636 --- /dev/null +++ b/Tests/EmbraceCoreTests/TestSupport/Records/LogRecord+Init.swift @@ -0,0 +1,46 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import EmbraceStorageInternal +import OpenTelemetryApi + +extension LogRecord { + convenience init( + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date = Date(), + attributes: [String: AttributeValue] + ) { + self.init() + + self.idRaw = id.toString + self.processIdRaw = processId.hex + self.severityRaw = severity.rawValue + self.body = body + self.timestamp = timestamp + + for (key, value) in attributes { + let attribute = LogAttributeRecord(key: key, value: value, log: self) + self.attributes.append(attribute) + } + } +} + +extension LogAttributeRecord { + convenience init( + key: String, + value: AttributeValue, + log: LogRecord + ) { + self.init() + + self.key = key + self.valueRaw = value.description + self.log = log + } +} diff --git a/Tests/EmbraceCoreTests/TestSupport/Records/MetadataRecord+Init.swift b/Tests/EmbraceCoreTests/TestSupport/Records/MetadataRecord+Init.swift new file mode 100644 index 00000000..8a2f4a61 --- /dev/null +++ b/Tests/EmbraceCoreTests/TestSupport/Records/MetadataRecord+Init.swift @@ -0,0 +1,29 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import EmbraceStorageInternal + +extension MetadataRecord { + convenience init( + key: String, + value: String, + type: MetadataRecordType, + lifespan: MetadataRecordLifespan, + lifespanId: String, + collectedAt: Date = Date() + ) { + self.init() + + self.key = key + self.value = value + self.typeRaw = type.rawValue + self.lifespanRaw = lifespan.rawValue + self.lifespanId = lifespanId + self.collectedAt = collectedAt + } +} + + diff --git a/Tests/EmbraceCoreTests/TestSupport/Records/SessionRecord+Init.swift b/Tests/EmbraceCoreTests/TestSupport/Records/SessionRecord+Init.swift new file mode 100644 index 00000000..f31fa720 --- /dev/null +++ b/Tests/EmbraceCoreTests/TestSupport/Records/SessionRecord+Init.swift @@ -0,0 +1,39 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import EmbraceStorageInternal + +extension SessionRecord { + convenience init( + id: SessionIdentifier, + processId: ProcessIdentifier, + state: SessionState, + traceId: String, + spanId: String, + startTime: Date, + endTime: Date? = nil, + lastHeartbeatTime: Date? = nil, + crashReportId: String? = nil, + coldStart: Bool = false, + cleanExit: Bool = false, + appTerminated: Bool = false + ) { + self.init() + + self.idRaw = id.toString + self.processIdRaw = processId.hex + self.state = state.rawValue + self.traceId = traceId + self.spanId = spanId + self.startTime = startTime + self.endTime = endTime + self.lastHeartbeatTime = ((lastHeartbeatTime ?? endTime) ?? startTime) + self.crashReportId = crashReportId + self.coldStart = coldStart + self.cleanExit = cleanExit + self.appTerminated = appTerminated + } +} diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift index 715d0c69..1e76dc7a 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift @@ -13,17 +13,17 @@ class MockMetadataFetcher: EmbraceStorageMetadataFetcher { self.metadata = metadata } - func fetchAllResources() throws -> [MetadataRecord] { + func fetchAllResources() -> [MetadataRecord] { return metadata } - func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { return metadata.filter { record in (record.type == .resource || record.type == .requiredResource) } } - func fetchResourcesForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] { + func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { return metadata.filter { record in (record.type == .resource || record.type == .requiredResource) && record.lifespan == .process && @@ -31,7 +31,7 @@ class MockMetadataFetcher: EmbraceStorageMetadataFetcher { } } - func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { return metadata.filter { record in record.type == .customProperty && record.lifespan == .session && @@ -39,7 +39,7 @@ class MockMetadataFetcher: EmbraceStorageMetadataFetcher { } } - func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { return metadata.filter { record in record.type == .personaTag && record.lifespan == .session && @@ -47,7 +47,7 @@ class MockMetadataFetcher: EmbraceStorageMetadataFetcher { } } - func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] { + func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { return metadata.filter { record in record.type == .personaTag && record.lifespan == .process && diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift index 751cc77d..d0bf6cf1 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift @@ -18,6 +18,7 @@ class MockSessionController: SessionControllable { private var updateSessionCallback: ((SessionRecord?, SessionState?, Bool?) -> Void)? + weak var storage: EmbraceStorage? var currentSession: SessionRecord? func clear() { } @@ -33,10 +34,10 @@ class MockSessionController: SessionControllable { endSession() } - let session = SessionRecord( + let session = storage?.addSession( id: nextSessionId ?? .random, - state: state, processId: ProcessIdentifier.current, + state: state, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: startTime diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift index 011ce139..73d59adb 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift @@ -2,33 +2,48 @@ // Copyright © 2023 Embrace Mobile, Inc. All rights reserved. // +import Foundation import EmbraceStorageInternal import EmbraceCommonInternal +import OpenTelemetryApi class SpyLogRepository: LogRepository { + var didCallFetchAll = false var stubbedFetchAllResult: [LogRecord] = [] - func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) throws -> [LogRecord] { + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [LogRecord] { didCallFetchAll = true return stubbedFetchAllResult } var didCallRemoveLogs = false - func remove(logs: [LogRecord]) throws { + func remove(logs: [LogRecord]) { didCallRemoveLogs = true } var didCallRemoveAllLogs = false - func removeAllLogs() throws { + func removeAllLogs() { didCallRemoveAllLogs = true } - var didCallCreate: Bool = false - var stubbedCreateCompletionResult: (Result)? - func create(_ log: LogRecord, completion: (Result) -> Void) { + var didCallCreate = false + func createLog( + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date, + attributes: [String : AttributeValue] + ) -> LogRecord { didCallCreate = true - if let result = stubbedCreateCompletionResult { - completion(result) - } + + return LogRecord( + id: id, + processId: processId, + severity: severity, + body: body, + timestamp: timestamp, + attributes: attributes + ) } } diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift index 29bb987e..0475ee07 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift @@ -5,6 +5,7 @@ import Foundation import EmbraceStorageInternal import EmbraceCommonInternal +import OpenTelemetryApi class RandomError: Error, CustomNSError { static var errorDomain: String = "Embrace" @@ -13,118 +14,98 @@ class RandomError: Error, CustomNSError { } class SpyStorage: Storage { - private let shouldThrow: Bool - - init(shouldThrow: Bool = false) { - self.shouldThrow = shouldThrow - } var didCallFetchAllResources = false var stubbedFetchAllResources: [MetadataRecord] = [] - func fetchAllResources() throws -> [MetadataRecord] { + func fetchAllResources() -> [MetadataRecord] { didCallFetchAllResources = true - guard !shouldThrow else { - throw RandomError() - } return stubbedFetchAllResources } var didCallFetchResourcesForSessionId = false var fetchResourcesForSessionIdReceivedParameter: SessionIdentifier! var stubbedFetchResourcesForSessionId: [MetadataRecord] = [] - func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { didCallFetchResourcesForSessionId = true fetchResourcesForSessionIdReceivedParameter = sessionId - guard !shouldThrow else { - throw RandomError() - } return stubbedFetchResourcesForSessionId } var didCallFetchResourcesForProcessId = false var fetchResourcesForProcessIdReceivedParameter: ProcessIdentifier! var stubbedFetchResourcesForProcessId: [MetadataRecord] = [] - func fetchResourcesForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] { + func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { didCallFetchResourcesForProcessId = true fetchResourcesForProcessIdReceivedParameter = processId - guard !shouldThrow else { - throw RandomError() - } return stubbedFetchResourcesForProcessId } var didCallFetchCustomPropertiesForSessionId = false var fetchCustomPropertiesForSessionIdReceivedParameter: SessionIdentifier! var stubbedFetchCustomPropertiesForSessionId: [MetadataRecord] = [] - func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { didCallFetchCustomPropertiesForSessionId = true fetchCustomPropertiesForSessionIdReceivedParameter = sessionId - guard !shouldThrow else { - throw RandomError() - } return stubbedFetchCustomPropertiesForSessionId } var didCallFetchPersonaTagsForSessionId = false var fetchPersonaTagsForSessionIdReceivedParameter: SessionIdentifier! var stubbedFetchPersonaTagsForSessionId: [MetadataRecord] = [] - func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { didCallFetchPersonaTagsForSessionId = true fetchPersonaTagsForSessionIdReceivedParameter = sessionId - guard !shouldThrow else { - throw RandomError() - } return stubbedFetchPersonaTagsForSessionId } var didCallFetchPersonaTagsForProcessId = false var fetchPersonaTagsForProcessIdReceivedParameter: ProcessIdentifier! var stubbedFetchPersonaTagsForProcessId: [MetadataRecord] = [] - func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) throws -> [MetadataRecord] { + func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { didCallFetchPersonaTagsForProcessId = true fetchPersonaTagsForProcessIdReceivedParameter = processId - guard !shouldThrow else { - throw RandomError() - } return stubbedFetchPersonaTagsForProcessId } var didCallCreate = false - var stubbedCreateResult: Result? - func create(_ log: LogRecord, completion: (Result) -> Void) { + func createLog( + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date, + attributes: [String : AttributeValue] + ) -> LogRecord { didCallCreate = true - if let result = stubbedCreateResult { - completion(result) - } + + return LogRecord( + id: id, + processId: processId, + severity: severity, + body: body, + timestamp: timestamp, + attributes: attributes + ) } var didCallFetchAllExcludingProcessIdentifier = false var stubbedFetchAllExcludingProcessIdentifier: [LogRecord] = [] var fetchAllExcludingProcessIdentifierReceivedParameter: ProcessIdentifier! - func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) throws -> [LogRecord] { + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [LogRecord] { didCallFetchAllExcludingProcessIdentifier = true - guard !shouldThrow else { - throw RandomError() - } fetchAllExcludingProcessIdentifierReceivedParameter = processIdentifier return stubbedFetchAllExcludingProcessIdentifier } var didCallRemoveLogs = false var removeLogsReceivedParameter: [LogRecord] = [] - func remove(logs: [LogRecord]) throws { + func remove(logs: [LogRecord]) { didCallRemoveLogs = true removeLogsReceivedParameter = logs - guard !shouldThrow else { - throw RandomError() - } } var didCallRemoveAllLogs = false - func removeAllLogs() throws { + func removeAllLogs() { didCallRemoveAllLogs = true - guard !shouldThrow else { - throw RandomError() - } } } diff --git a/Tests/EmbraceCoreTests/TestSupport/Utilities/MetadataRecord+Factory.swift b/Tests/EmbraceCoreTests/TestSupport/Utilities/MetadataRecord+Factory.swift index 8c54b986..d2b461c4 100644 --- a/Tests/EmbraceCoreTests/TestSupport/Utilities/MetadataRecord+Factory.swift +++ b/Tests/EmbraceCoreTests/TestSupport/Utilities/MetadataRecord+Factory.swift @@ -13,9 +13,9 @@ extension MetadataRecord { value: AttributeValue, sessionId: SessionIdentifier = .random ) -> MetadataRecord { - self.init( + MetadataRecord( key: key, - value: value, + value: value.description, type: .customProperty, lifespan: .session, lifespanId: sessionId.toString @@ -23,9 +23,9 @@ extension MetadataRecord { } static func userMetadata(key: String, value: String) -> MetadataRecord { - .init( + MetadataRecord( key: key, - value: .string(value), + value: value, type: .customProperty, lifespan: .session, lifespanId: .random() @@ -33,9 +33,9 @@ extension MetadataRecord { } static func createResourceRecord(key: String, value: String) -> MetadataRecord { - .init( + MetadataRecord( key: key, - value: .string(value), + value: value, type: .resource, lifespan: .session, lifespanId: .random() @@ -43,9 +43,9 @@ extension MetadataRecord { } static func createPersonaTagRecord(value: String) -> MetadataRecord { - .init( + MetadataRecord( key: value, - value: .string(value), + value: value, type: .personaTag, lifespan: .session, lifespanId: .random() diff --git a/Tests/EmbraceCoreTests/TestSupport/Utilities/SessionRecord+Factory.swift b/Tests/EmbraceCoreTests/TestSupport/Utilities/SessionRecord+Factory.swift index a2e689d9..0637f990 100644 --- a/Tests/EmbraceCoreTests/TestSupport/Utilities/SessionRecord+Factory.swift +++ b/Tests/EmbraceCoreTests/TestSupport/Utilities/SessionRecord+Factory.swift @@ -8,10 +8,10 @@ import EmbraceCommonInternal extension SessionRecord { static func with(id: SessionIdentifier, state: SessionState) -> SessionRecord { - .init( + SessionRecord( id: id, - state: state, processId: .random, + state: state, traceId: "", spanId: "", startTime: Date() diff --git a/Tests/EmbraceOTelInternalTests/Trace/Tracer/Span/Processor/SingleSpanProcessorTests.swift b/Tests/EmbraceOTelInternalTests/Trace/Tracer/Span/Processor/SingleSpanProcessorTests.swift index faa55188..9f90ac37 100644 --- a/Tests/EmbraceOTelInternalTests/Trace/Tracer/Span/Processor/SingleSpanProcessorTests.swift +++ b/Tests/EmbraceOTelInternalTests/Trace/Tracer/Span/Processor/SingleSpanProcessorTests.swift @@ -71,7 +71,7 @@ final class SingleSpanProcessorTests: XCTestCase { expectation.fulfill() } - let span = createSpanData(processor: processor) // DEV: `startSpan` called in this method + _ = createSpanData(processor: processor) // DEV: `startSpan` called in this method wait(for: [expectation], timeout: .defaultTimeout) XCTAssertEqual(exporter.exportedSpans.count, 0) diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift index ff5fac2c..65540aea 100644 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift +++ b/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift @@ -15,152 +15,88 @@ class EmbraceStorageLoggingTests: XCTestCase { } override func tearDownWithError() throws { - try sut.teardown() + sut.coreData.destroy() } // MARK: - CreateLog func test_createLog_shouldCreateItInDataBase() throws { - let expectation = expectation(description: #function) - let log = LogRecord(identifier: .random, - processIdentifier: .random, - severity: .info, - body: "log message", - attributes: .empty() + let id = LogIdentifier.random + sut.createLog( + id: id, + processId: .random, + severity: .info, + body: "log message", + attributes: .empty() ) - sut.create(log) { result in - if case let Result.success(persistedLog) = result { - XCTAssertEqual(log.identifier, persistedLog.identifier) - } else { - XCTFail("Couldn't persist log") - } - - do { - try sut.dbQueue.read { db in - XCTAssertTrue(try log.exists(db)) - expectation.fulfill() - } - } catch let exception { - XCTFail(exception.localizedDescription) - } - } - - wait(for: [expectation], timeout: .defaultTimeout) - } - - // MARK: - GetAll - - func testFilledDb_getAll_shouldGetAlllogsFromDatabase() throws { - let logs: [LogRecord] = [.infoLog(), .infoLog(), .infoLog(), .infoLog()] - givenDatabase(withLogs: logs) - - let result = try sut.getAll() - - XCTAssertEqual(result, logs) - } - - func testEmptyDb_getAll_shouldGetAlllogsFromDatabase() throws { - givenDatabase(withLogs: []) - - let result = try sut.getAll() - - XCTAssertTrue(result.isEmpty) + let logs: [LogRecord] = sut.fetchAll() + XCTAssertEqual(logs.count, 1) + XCTAssertNotNil(logs.first(where: { $0.idRaw == id.toString })) } // MARK: - Fetch All Excluding Process Identifier func test_fetchAllExcludingProcessIdentifier_shouldFilterLogsProperly() throws { let pid = ProcessIdentifier(value: 12345) - let log1 = LogRecord.infoLog(pid: pid) - let log2 = LogRecord.infoLog(pid: pid) - givenDatabase(withLogs: [ - .infoLog(), - log1, - .infoLog(), - log2 - ]) + createInfoLog(pid: pid) + createInfoLog() + createInfoLog(pid: pid) + createInfoLog() - let result = try sut.fetchAll(excludingProcessIdentifier: pid) + let result = sut.fetchAll(excludingProcessIdentifier: pid) XCTAssertEqual(result.count, 2) - XCTAssertTrue(!result.contains(where: { $0.processIdentifier == pid })) + XCTAssertTrue(!result.contains(where: { $0.processIdRaw == pid.hex })) } // MARK: - RemoveAllLogs func testFilledDb_removeAllLogs_shouldCleanDb() throws { - let expectation = expectation(description: #function) - let logs: [LogRecord] = [.infoLog(), .infoLog(), .infoLog()] - givenDatabase(withLogs: logs) - - try sut.removeAllLogs() - - try sut.dbQueue.read { db in - try logs.forEach { - XCTAssertFalse(try $0.exists(db)) - } - expectation.fulfill() - } - wait(for: [expectation]) + createInfoLog() + createInfoLog() + createInfoLog() + + sut.removeAllLogs() + + let logs: [LogRecord] = sut.fetchAll() + XCTAssertEqual(logs.count, 0) } // MARK: - Remove Specific Logs func testFilledDb_removeSpecificLog_shouldDeleteJustTheSpecificLog() throws { - let expectation = expectation(description: #function) - let firstLogToDelete: LogRecord = .infoLog() - let secondLogToDelete: LogRecord = .infoLog() - let nonDeletedLog: LogRecord = .infoLog() - let logs: [LogRecord] = [ - firstLogToDelete, - secondLogToDelete, - nonDeletedLog - ] + let uuid1 = UUID() + let firstLogToDelete = createInfoLog(withId: uuid1) + + let uuid2 = UUID() + let secondLogToDelete = createInfoLog(withId: uuid2) - givenDatabase(withLogs: logs) + let uuid3 = UUID() + createInfoLog(withId: uuid3) - try sut.remove(logs: [ + sut.remove(logs: [ firstLogToDelete, secondLogToDelete ]) - try sut.dbQueue.read { db in - XCTAssertFalse(try firstLogToDelete.exists(db)) - XCTAssertFalse(try secondLogToDelete.exists(db)) - XCTAssertTrue(try nonDeletedLog.exists(db)) - expectation.fulfill() - } - wait(for: [expectation]) + let logs: [LogRecord] = sut.fetchAll() + XCTAssertEqual(logs.count, 1) + XCTAssertNil(logs.first(where: { $0.idRaw == uuid1.withoutHyphen })) + XCTAssertNil(logs.first(where: { $0.idRaw == uuid2.withoutHyphen })) + XCTAssertNotNil(logs.first(where: { $0.idRaw == uuid3.withoutHyphen })) } } private extension EmbraceStorageLoggingTests { - func givenDatabase(withLogs logs: [LogRecord]) { - do { - try logs.forEach { log in - try sut.dbQueue.write { db in - try log.insert(db) - } - } - } catch let exception { - XCTFail("Couldn't create logs: \(exception.localizedDescription)") - } - } -} - -extension LogRecord { - static func infoLog(withId id: UUID = UUID(), pid: ProcessIdentifier = .random) -> LogRecord { - .init(identifier: LogIdentifier.init(value: id), - processIdentifier: pid, - severity: .info, - body: "a log message", - attributes: .empty()) - } -} - -extension LogRecord: Equatable { - public static func == (lhs: LogRecord, rhs: LogRecord) -> Bool { - lhs.identifier == rhs.identifier + @discardableResult + func createInfoLog(withId id: UUID = UUID(), pid: ProcessIdentifier = .random) -> LogRecord { + return sut.createLog( + id: LogIdentifier.init(value: id), + processId: pid, + severity: .info, + body: "a log message", + attributes: .empty() + ) } } diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageOptionsTests.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageOptionsTests.swift deleted file mode 100644 index 7df23f2f..00000000 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageOptionsTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest -@testable import EmbraceStorageInternal - -class EmbraceStorageOptionsTests: XCTestCase { - - func test_init_withBaseURLAndFileName() { - let url = URL(fileURLWithPath: NSTemporaryDirectory()) - let options = EmbraceStorage.Options(baseUrl: url, fileName: "test.sqlite") - - XCTAssertNil(options.name) - XCTAssertEqual(options.baseUrl, url) - XCTAssertEqual(options.fileName, "test.sqlite") - XCTAssertEqual(options.fileURL, url.appendingPathComponent("test.sqlite")) - } - - func test_init_withName() { - let options = EmbraceStorage.Options(named: "example") - - XCTAssertEqual(options.name, "example") - XCTAssertNil(options.baseUrl) - XCTAssertNil(options.fileName) - XCTAssertNil(options.fileURL) - } -} diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift index 01766c42..8dfa959b 100644 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift +++ b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift @@ -4,7 +4,6 @@ import XCTest import TestSupport -import GRDB @testable import EmbraceStorageInternal class EmbraceStorageTests: XCTestCase { @@ -15,149 +14,12 @@ class EmbraceStorageTests: XCTestCase { } override func tearDownWithError() throws { - try storage.teardown() - } - - func test_databaseSchema() throws { - // then all required tables should be present - try storage.dbQueue.read { db in - XCTAssertTrue(try db.tableExists(SessionRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(SpanRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(MetadataRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(LogRecord.databaseTableName)) - } - } - - func test_performMigration_generatesTables() throws { - storage = try EmbraceStorage.createInMemoryDb(runMigrations: false) - - try storage.dbQueue.read { db in - XCTAssertFalse(try db.tableExists(SessionRecord.databaseTableName)) - XCTAssertFalse(try db.tableExists(SpanRecord.databaseTableName)) - XCTAssertFalse(try db.tableExists(MetadataRecord.databaseTableName)) - XCTAssertFalse(try db.tableExists(LogRecord.databaseTableName)) - } - - try storage.performMigration() - - try storage.dbQueue.read { db in - XCTAssertTrue(try db.tableExists(SessionRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(SpanRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(MetadataRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(LogRecord.databaseTableName)) - } - } - - func test_performMigration_ifResetsIfErrorTrue_resetsDB() throws { - storage = try EmbraceStorage.createInMemoryDb() - - let migration = ThrowingMigration(performToThrow: 1) - try storage.performMigration(resetIfError: true, migrations: .current + [migration]) - - try storage.dbQueue.read { db in - XCTAssertTrue(try db.tableExists(SessionRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(SpanRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(MetadataRecord.databaseTableName)) - XCTAssertTrue(try db.tableExists(LogRecord.databaseTableName)) - } - - XCTAssertEqual(migration.currentPerformCount, 2) - } - - func test_performMigration_ifResetsIfErrorTrue_andMigrationFailsTwice_rethrowsError() throws { - storage = try EmbraceStorage.createInMemoryDb() - - let migration = ThrowingMigration(performsToThrow: [1, 2]) - XCTAssertThrowsError( - try storage.performMigration( - resetIfError: true, - migrations: [migration] - ) - ) - - XCTAssertEqual(migration.currentPerformCount, 2) - } - - func test_performMigration_ifResetsIfErrorFalse_rethrowsError() throws { - storage = try EmbraceStorage.createInMemoryDb() - let migration = ThrowingMigration(performToThrow: 1) - - XCTAssertThrowsError( - try storage.performMigration( - resetIfError: false, - migrations: [migration] - ) - ) - XCTAssertEqual(migration.currentPerformCount, 1) - } - - func test_reset_remakesDB() throws { - storage = try .createInDiskDb() // need to use on disk DB, - // inMemory will keep same memory instance because dbQueue `name` is the same. - - // given inserted record - let span = SpanRecord( - id: "id", - name: "a name", - traceId: "traceId", - type: .performance, - data: Data(), - startTime: Date() - ) - - try storage.dbQueue.write { db in - try span.insert(db) - } - - try storage.reset() - - // then record should not exist in storage - try storage.dbQueue.read { db in - XCTAssertFalse(try span.exists(db)) - } - - try FileManager.default.removeItem(at: storage.options.fileURL!) - } - -// MARK: - DB actions - - func test_update() throws { - // given inserted record - var span = SpanRecord( - id: "id", - name: "a name", - traceId: "traceId", - type: .performance, - data: Data(), - startTime: Date() - ) - - try storage.dbQueue.write { db in - try span.insert(db) - } - - // then record should exist in storage - try storage.dbQueue.read { db in - XCTAssert(try span.exists(db)) - } - - // when updating record - let endTime = Date(timeInterval: 10, since: span.startTime) - span.endTime = endTime - - try storage.update(record: span) - - // the record should update successfully - try storage.dbQueue.read { db in - XCTAssert(try span.exists(db)) - XCTAssertNotNil(span.endTime) - XCTAssertEqual(span.endTime, endTime) - } + storage.coreData.destroy() } func test_delete() throws { // given inserted record - let span = SpanRecord( + let span = storage.upsertSpan( id: "id", name: "a name", traceId: "traceId", @@ -166,28 +28,22 @@ class EmbraceStorageTests: XCTestCase { startTime: Date() ) - try storage.dbQueue.write { db in - try span.insert(db) - } - // then record should exist in storage - try storage.dbQueue.read { db in - XCTAssert(try span.exists(db)) - } + var spans: [SpanRecord] = storage.fetchAll() + XCTAssertEqual(spans.count, 1) + XCTAssertNotNil(spans.first(where: { $0.name == "a name"})) // when deleting record - let success = try storage.delete(record: span) - XCTAssert(success) + storage.delete(span) // then record should not exist in storage - try storage.dbQueue.read { db in - XCTAssertFalse(try span.exists(db)) - } + spans = storage.fetchAll() + XCTAssertEqual(spans.count, 0) } func test_fetchAll() throws { // given inserted records - let span1 = SpanRecord( + let span1 = storage.upsertSpan( id: "id1", name: "a name 1", traceId: "traceId", @@ -195,7 +51,7 @@ class EmbraceStorageTests: XCTestCase { data: Data(), startTime: Date() ) - let span2 = SpanRecord( + let span2 = storage.upsertSpan( id: "id2", name: "a name 2", traceId: "traceId", @@ -204,91 +60,12 @@ class EmbraceStorageTests: XCTestCase { startTime: Date() ) - try storage.dbQueue.write { db in - try span1.insert(db) - try span2.insert(db) - } - - // then records should exist in storage - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try span1.exists(db)) - XCTAssert(try span2.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) - // when fetching all records - let records: [SpanRecord] = try storage.fetchAll() + let records: [SpanRecord] = storage.fetchAll() // then all records should be successfully fetched XCTAssert(records.count == 2) XCTAssert(records.contains(span1)) XCTAssert(records.contains(span2)) } - - func test_corruptedDbAction() { - guard let corruptedDb = EmbraceStorageTests.prepareCorruptedDBForTest() else { - return XCTFail("\(#function): Failed to create corrupted DB for test") - } - - let dbBaseUrl = corruptedDb.deletingLastPathComponent() - let dbFile = corruptedDb.lastPathComponent - - /// Make sure the target DB is corrupted and GRDB returns the expected result when trying to load it. - let corruptedAttempt = Result(catching: { try DatabaseQueue(path: corruptedDb.absoluteString) }) - if case let .failure(error as DatabaseError) = corruptedAttempt { - XCTAssertEqual(error.resultCode, .SQLITE_CORRUPT) - } else { - XCTFail("\(#function): Failed to load a corrupted db for test.") - } - - /// Attempting to create an EmbraceStorage with the corrupted DB should result in a valid storage creation - let storeCreationAttempt = Result(catching: { - try EmbraceStorage(options: .init(baseUrl: dbBaseUrl, fileName: dbFile), logger: MockLogger()) - }) - if case let .failure(error) = storeCreationAttempt { - XCTFail("\(#function): EmbraceStorage failed to recover from corrupted existing DB: \(error)") - } - - /// Then the corrupted DB should've been corrected and now GRDB should be able to load it. - let fixedAttempt = Result(catching: { try DatabaseQueue(path: corruptedDb.absoluteString) }) - if case let .failure(error) = fixedAttempt { - XCTFail("\(#function): DB Is still corrupted after it should've been fixed: \(error)") - } - } - - static func prepareCorruptedDBForTest() -> URL? { - guard - let resourceUrl = Bundle.module.path(forResource: "db_corrupted", ofType: "sqlite", inDirectory: "Mocks"), - let corruptedDbPath = URL(string: "file://\(resourceUrl)") - else { - return nil - } - - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()) - let copyCorruptedPath = temporaryDirectoryURL.appendingPathComponent("db.sqlite") - - do { - if !FileManager.default.fileExists(atPath: temporaryDirectoryURL.path, isDirectory: nil) { - try FileManager.default.createDirectory( - at: temporaryDirectoryURL, - withIntermediateDirectories: true, - attributes: nil - ) - } - - if FileManager.default.fileExists(atPath: copyCorruptedPath.path) { - try FileManager.default.removeItem(at: copyCorruptedPath) - } - - try FileManager.default.copyItem(at: corruptedDbPath, to: copyCorruptedPath) - return copyCorruptedPath - } catch let e { - print("\(#function): error creating corrupt db: \(e)") - return nil - } - - } } diff --git a/Tests/EmbraceStorageInternalTests/FetchMethods/EmbraceStorage+SpanForSessionRecordTests.swift b/Tests/EmbraceStorageInternalTests/FetchMethods/EmbraceStorage+SpanForSessionRecordTests.swift index be255879..0a584467 100644 --- a/Tests/EmbraceStorageInternalTests/FetchMethods/EmbraceStorage+SpanForSessionRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/FetchMethods/EmbraceStorage+SpanForSessionRecordTests.swift @@ -24,7 +24,7 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { } override func tearDownWithError() throws { - try storage.teardown() + storage.coreData.destroy() } func addSpanRecord( @@ -33,8 +33,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { processIdentifier: ProcessIdentifier = .current, startTime: Date, endTime: Date? = nil - ) throws -> SpanRecord { - let span = SpanRecord( + ) -> SpanRecord { + return storage.upsertSpan( id: SpanId.random().hexString, name: name, traceId: TraceId.random().hexString, @@ -44,9 +44,6 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: endTime, processIdentifier: processIdentifier ) - try storage.upsertSpan(span) - - return span } func sessionRecord( @@ -58,10 +55,10 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { traceId: TraceId = .random(), spanId: SpanId = .random() ) -> SessionRecord { - return SessionRecord( + return storage.addSession( id: .random, - state: .foreground, processId: processIdentifier, + state: .foreground, traceId: traceId.hexString, spanId: spanId.hexString, startTime: startTime, @@ -80,7 +77,7 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { startTime: .relative(-20), endTime: .relative(-5) ) - let results = try storage.fetchSpans(for: session) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.isEmpty) } @@ -92,8 +89,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-5) ) - _ = try addSpanRecord(startTime: .relative(-30), endTime: .relative(-25)) - let results = try storage.fetchSpans(for: session) + _ = addSpanRecord(startTime: .relative(-30), endTime: .relative(-25)) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.isEmpty) } @@ -107,8 +104,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { coldStart: true ) - let span = try addSpanRecord(startTime: .relative(-30), endTime: .relative(-25)) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-30), endTime: .relative(-25)) + let results = storage.fetchSpans(for: session) XCTAssertEqual(results.count, 1) XCTAssertTrue(results.contains(span)) @@ -122,8 +119,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-5) ) - _ = try addSpanRecord(startTime: .relative(-2), endTime: Date()) - let results = try storage.fetchSpans(for: session) + _ = addSpanRecord(startTime: .relative(-2), endTime: Date()) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.isEmpty) } @@ -136,8 +133,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-5) ) - let span = try addSpanRecord(startTime: .relative(-22), endTime: .relative(-18)) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-22), endTime: .relative(-18)) + let results = storage.fetchSpans(for: session) XCTAssertEqual(results.count, 1) XCTAssertTrue(results.contains(span)) @@ -151,8 +148,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-5) ) - let span = try addSpanRecord(startTime: .relative(-7), endTime: .relative(-2)) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-7), endTime: .relative(-2)) + let results = storage.fetchSpans(for: session) XCTAssertEqual(results.count, 1) XCTAssertTrue(results.contains(span)) @@ -166,8 +163,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-10) ) - let span = try addSpanRecord(startTime: .relative(-25), endTime: .relative(-5)) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-25), endTime: .relative(-5)) + let results = storage.fetchSpans(for: session) XCTAssertEqual(results.count, 1) XCTAssertTrue(results.contains(span)) @@ -182,8 +179,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-5) ) - let span = try addSpanRecord(startTime: .relative(-22), endTime: nil) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-22), endTime: nil) + let results = storage.fetchSpans(for: session) XCTAssertEqual(results.count, 1) XCTAssertTrue(results.contains(span)) @@ -197,8 +194,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-5) ) - let span = try addSpanRecord(startTime: .relative(-10), endTime: nil) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-10), endTime: nil) + let results = storage.fetchSpans(for: session) XCTAssertEqual(results.count, 1) XCTAssertTrue(results.contains(span)) @@ -213,8 +210,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { coldStart: true ) - let span = try addSpanRecord(startTime: .relative(-22), endTime: nil) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-22), endTime: nil) + let results = storage.fetchSpans(for: session) XCTAssertEqual(results.count, 1) XCTAssertTrue(results.contains(span)) @@ -229,8 +226,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { coldStart: true ) - let span = try addSpanRecord(startTime: .relative(-10), endTime: nil) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-10), endTime: nil) + let results = storage.fetchSpans(for: session) XCTAssertEqual(results.count, 1) XCTAssertTrue(results.contains(span)) @@ -245,8 +242,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { coldStart: true ) - _ = try addSpanRecord(startTime: .relative(-2), endTime: nil) - let results = try storage.fetchSpans(for: session) + _ = addSpanRecord(startTime: .relative(-2), endTime: nil) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.isEmpty) } @@ -261,10 +258,10 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { coldStart: true ) - let spanA = try addSpanRecord(name: "span-a", startTime: .relative(-22), endTime: .relative(-18)) - let spanB = try addSpanRecord(name: "span-b", startTime: .relative(-16), endTime: .relative(-12)) - let spanC = try addSpanRecord(name: "span-c", startTime: .relative(-6), endTime: .relative(-2)) - let results = try storage.fetchSpans(for: session) + let spanA = addSpanRecord(name: "span-a", startTime: .relative(-22), endTime: .relative(-18)) + let spanB = addSpanRecord(name: "span-b", startTime: .relative(-16), endTime: .relative(-12)) + let spanC = addSpanRecord(name: "span-c", startTime: .relative(-6), endTime: .relative(-2)) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.contains(spanA)) XCTAssertTrue(results.contains(spanB)) @@ -280,10 +277,10 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { coldStart: true ) - let spanA = try addSpanRecord(name: "span-a", startTime: .relative(-28), endTime: .relative(-22)) - let spanB = try addSpanRecord(name: "span-b", startTime: .relative(-16), endTime: .relative(-12)) - let spanC = try addSpanRecord(name: "span-c", startTime: .relative(-6), endTime: .relative(-2)) - let results = try storage.fetchSpans(for: session) + let spanA = addSpanRecord(name: "span-a", startTime: .relative(-28), endTime: .relative(-22)) + let spanB = addSpanRecord(name: "span-b", startTime: .relative(-16), endTime: .relative(-12)) + let spanC = addSpanRecord(name: "span-c", startTime: .relative(-6), endTime: .relative(-2)) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.contains(spanA)) XCTAssertTrue(results.contains(spanB)) @@ -299,10 +296,10 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { coldStart: false ) - let spanA = try addSpanRecord(name: "span-a", startTime: .relative(-28), endTime: .relative(-22)) - let spanB = try addSpanRecord(name: "span-b", startTime: .relative(-16), endTime: .relative(-12)) - let spanC = try addSpanRecord(name: "span-c", startTime: .relative(-6), endTime: .relative(-2)) - let results = try storage.fetchSpans(for: session) + let spanA = addSpanRecord(name: "span-a", startTime: .relative(-28), endTime: .relative(-22)) + let spanB = addSpanRecord(name: "span-b", startTime: .relative(-16), endTime: .relative(-12)) + let spanC = addSpanRecord(name: "span-c", startTime: .relative(-6), endTime: .relative(-2)) + let results = storage.fetchSpans(for: session) XCTAssertFalse(results.contains(spanA)) XCTAssertTrue(results.contains(spanB)) @@ -320,8 +317,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-5) ) - let span = try addSpanRecord(startTime: .relative(-30), endTime: boundary) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-30), endTime: boundary) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.contains(span)) } @@ -337,8 +334,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { coldStart: true ) - let span = try addSpanRecord(startTime: .relative(-30), endTime: boundary) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: .relative(-30), endTime: boundary) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.contains(span)) } @@ -353,8 +350,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: boundary ) - let span = try addSpanRecord(startTime: boundary, endTime: .relative(0)) - let results = try storage.fetchSpans(for: session) + let span = addSpanRecord(startTime: boundary, endTime: .relative(0)) + let results = storage.fetchSpans(for: session) XCTAssertTrue(results.contains(span)) } @@ -367,13 +364,13 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-10) ) - _ = try addSpanRecord( + _ = addSpanRecord( type: .session, startTime: session.startTime, endTime: session.endTime ) - let results = try storage.fetchSpans(for: session, ignoreSessionSpans: true) + let results = storage.fetchSpans(for: session, ignoreSessionSpans: true) XCTAssertTrue(results.isEmpty) } @@ -385,13 +382,13 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: .relative(-10) ) - let span = try addSpanRecord( + let span = addSpanRecord( type: .session, startTime: session.startTime, endTime: session.endTime ) - let results = try storage.fetchSpans(for: session, ignoreSessionSpans: false) + let results = storage.fetchSpans(for: session, ignoreSessionSpans: false) XCTAssertTrue(results.contains(span)) } } diff --git a/Tests/EmbraceStorageInternalTests/MetadataRecordAttributeValueTests.swift b/Tests/EmbraceStorageInternalTests/MetadataRecordAttributeValueTests.swift deleted file mode 100644 index 8328faa6..00000000 --- a/Tests/EmbraceStorageInternalTests/MetadataRecordAttributeValueTests.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest - -@testable import EmbraceStorageInternal -import OpenTelemetryApi - -final class MetadataRecordAttributeValueTests: XCTestCase { - - func record(value: AttributeValue) -> MetadataRecord { - return MetadataRecord(key: "example", value: value, type: .resource, lifespan: .permanent, lifespanId: "") - } - - func test_bool_value() throws { - let record = record(value: .bool(true)) - XCTAssertEqual(record.value, .bool(true)) - - XCTAssertTrue(record.boolValue!) - XCTAssertEqual(record.integerValue, 1) - XCTAssertEqual(record.doubleValue, Double(1)) - XCTAssertEqual(record.stringValue, "true") - XCTAssertNil(record.uuidValue) - } - - func test_bool_value_when_false() throws { - let record = record(value: .bool(false)) - XCTAssertEqual(record.value, .bool(false)) - - XCTAssertFalse(record.boolValue!) - XCTAssertEqual(record.integerValue, 0) - XCTAssertEqual(record.doubleValue, Double(0)) - XCTAssertEqual(record.stringValue, "false") - XCTAssertNil(record.uuidValue) - } - - func test_integer_value() throws { - let record = record(value: .int(42)) - XCTAssertEqual(record.value, .int(42)) - - XCTAssertTrue(record.boolValue!) - XCTAssertEqual(record.integerValue, 42) - XCTAssertEqual(record.stringValue, "42") - XCTAssertEqual(record.doubleValue, Double(42)) - XCTAssertNil(record.uuidValue) - } - - func test_integer_value_when_0() throws { - let record = record(value: .int(0)) - XCTAssertEqual(record.value, .int(0)) - - XCTAssertFalse(record.boolValue!) - XCTAssertEqual(record.integerValue, 0) - XCTAssertEqual(record.doubleValue, Double(0)) - XCTAssertEqual(record.stringValue, "0") - XCTAssertNil(record.uuidValue) - } - - func test_integer_value_when_negative() throws { - let record = record(value: .int(-42)) - XCTAssertEqual(record.value, .int(-42)) - - XCTAssertFalse(record.boolValue!) - XCTAssertEqual(record.integerValue, -42) - XCTAssertEqual(record.doubleValue, Double(-42)) - XCTAssertEqual(record.stringValue, "-42") - XCTAssertNil(record.uuidValue) - } - - func test_double_value() throws { - let record = record(value: .double(42.2)) - XCTAssertEqual(record.value, .double(42.2)) - - XCTAssertTrue(record.boolValue!) - XCTAssertEqual(record.integerValue, 42) - XCTAssertEqual(record.doubleValue, Double(42.2)) - XCTAssertEqual(record.stringValue, "42.2") - XCTAssertNil(record.uuidValue) - } - - func test_double_value_when_0() throws { - let record = record(value: .double(0)) - XCTAssertEqual(record.value, .double(0)) - - XCTAssertFalse(record.boolValue!) - XCTAssertEqual(record.integerValue, 0) - XCTAssertEqual(record.doubleValue, Double(0)) - XCTAssertEqual(record.stringValue, "0.0") - XCTAssertNil(record.uuidValue) - } - - func test_double_value_when_negative() throws { - let record = record(value: .double(-42.2)) - XCTAssertEqual(record.value, .double(-42.2)) - - XCTAssertFalse(record.boolValue!) - XCTAssertEqual(record.integerValue, -42) - XCTAssertEqual(record.doubleValue, Double(-42.2)) - XCTAssertEqual(record.stringValue, "-42.2") - XCTAssertNil(record.uuidValue) - } - - func test_string_value() throws { - let record = record(value: .string("value")) - XCTAssertEqual(record.value, .string("value")) - - XCTAssertNil(record.boolValue) - XCTAssertNil(record.integerValue) - XCTAssertNil(record.doubleValue) - XCTAssertEqual(record.stringValue, "value") - XCTAssertNil(record.uuidValue) - } - - func test_string_value_when_numeric() throws { - let record = record(value: .string("42.2")) - XCTAssertEqual(record.value, .string("42.2")) - - XCTAssertNil(record.boolValue) - XCTAssertNil(record.integerValue) - XCTAssertEqual(record.doubleValue, 42.2) - XCTAssertEqual(record.stringValue, "42.2") - XCTAssertNil(record.uuidValue) - } - - func test_string_value_when_boolean() throws { - let record = record(value: .string("false")) - XCTAssertEqual(record.value, .string("false")) - - XCTAssertFalse(record.boolValue!) - XCTAssertNil(record.integerValue) - XCTAssertNil(record.doubleValue) - XCTAssertEqual(record.stringValue, "false") - XCTAssertNil(record.uuidValue) - } - - func test_uuid_value() throws { - let record = record(value: .string("53917E47-EA64-46AF-B6D9-B80D8677AB8B")) - XCTAssertEqual(record.value, .string("53917E47-EA64-46AF-B6D9-B80D8677AB8B")) - - XCTAssertNil(record.boolValue) - XCTAssertNil(record.integerValue) - XCTAssertNil(record.doubleValue) - XCTAssertEqual(record.stringValue, "53917E47-EA64-46AF-B6D9-B80D8677AB8B") - XCTAssertEqual(record.uuidValue, UUID(uuidString: "53917E47-EA64-46AF-B6D9-B80D8677AB8B")) - } - - func test_boolArray_value() throws { - let record = record(value: .boolArray([false, true])) - XCTAssertEqual(record.value, .boolArray([false, true])) - - XCTAssertNil(record.boolValue) - XCTAssertNil(record.integerValue) - XCTAssertNil(record.doubleValue) - XCTAssertNil(record.stringValue) - XCTAssertNil(record.uuidValue) - } - - func test_intArray_value() throws { - let record = record(value: .intArray([0, 1, 2])) - XCTAssertEqual(record.value, .intArray([0, 1, 2])) - - XCTAssertNil(record.boolValue) - XCTAssertNil(record.integerValue) - XCTAssertNil(record.doubleValue) - XCTAssertNil(record.stringValue) - XCTAssertNil(record.uuidValue) - } - - func test_doubleArray_value() throws { - let record = record(value: .doubleArray([0.2, 1.3, 2.4])) - XCTAssertEqual(record.value, .doubleArray([0.2, 1.3, 2.4])) - - XCTAssertNil(record.boolValue) - XCTAssertNil(record.integerValue) - XCTAssertNil(record.doubleValue) - XCTAssertNil(record.stringValue) - XCTAssertNil(record.uuidValue) - } - - func test_stringArray_value() throws { - let record = record(value: .stringArray(["foo", "bar"])) - XCTAssertEqual(record.value, .stringArray(["foo", "bar"])) - - XCTAssertNil(record.boolValue) - XCTAssertNil(record.integerValue) - XCTAssertNil(record.doubleValue) - XCTAssertNil(record.stringValue) - XCTAssertNil(record.uuidValue) - } -} diff --git a/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift b/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift index 201b6fec..a8415e0c 100644 --- a/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/MetadataRecordTests.swift @@ -15,106 +15,26 @@ class MetadataRecordTests: XCTestCase { } override func tearDownWithError() throws { - try storage.teardown() - } - - func test_tableSchema() throws { - XCTAssertEqual(MetadataRecord.databaseTableName, "metadata") - - // then the table and its colums should be correct - try storage.dbQueue.read { db in - XCTAssert(try db.tableExists(MetadataRecord.databaseTableName)) - - // primary key - XCTAssert(try db.table( - MetadataRecord.databaseTableName, - hasUniqueKey: [ - MetadataRecord.Schema.key.name, - MetadataRecord.Schema.type.name, - MetadataRecord.Schema.lifespan.name, - MetadataRecord.Schema.lifespanId.name - ] - )) - - // column count - let columns = try db.columns(in: MetadataRecord.databaseTableName) - XCTAssertEqual(columns.count, 6) - - // id - let keyColumn = columns.first(where: { $0.name == MetadataRecord.Schema.key.name }) - if let keyColumn = keyColumn { - XCTAssertEqual(keyColumn.type, "TEXT") - XCTAssert(keyColumn.isNotNull) - } else { - XCTAssert(false, "key column not found!") - } - - // state - let valueColumn = columns.first(where: { $0.name == MetadataRecord.Schema.value.name }) - if let valueColumn = valueColumn { - XCTAssertEqual(valueColumn.type, "TEXT") - XCTAssert(valueColumn.isNotNull) - } else { - XCTAssert(false, "value column not found!") - } - - // type - let typeColumn = columns.first(where: { $0.name == MetadataRecord.Schema.type.name }) - if let typeColumn = typeColumn { - XCTAssertEqual(typeColumn.type, "TEXT") - XCTAssert(typeColumn.isNotNull) - } else { - XCTAssert(false, "type column not found!") - } - - // collected_at - let collectedAtColumn = columns.first(where: { $0.name == MetadataRecord.Schema.collectedAt.name }) - if let collectedAtColumn = collectedAtColumn { - XCTAssertEqual(collectedAtColumn.type, "DATETIME") - XCTAssert(collectedAtColumn.isNotNull) - } else { - XCTAssert(false, "collected_at column not found!") - } - - // lifepsan - let lifespanColumn = columns.first(where: { $0.name == MetadataRecord.Schema.lifespan.name }) - if let lifespanColumn = lifespanColumn { - XCTAssertEqual(lifespanColumn.type, "TEXT") - XCTAssert(lifespanColumn.isNotNull) - } else { - XCTAssert(false, "lifespan column not found!") - } - - // lifepsan id - let lifespanIdColumn = columns.first(where: { $0.name == MetadataRecord.Schema.lifespanId.name }) - if let lifespanIdColumn = lifespanIdColumn { - XCTAssertEqual(lifespanIdColumn.type, "TEXT") - XCTAssert(lifespanIdColumn.isNotNull) - } else { - XCTAssert(false, "lifespan_id column not found!") - } - } + storage.coreData.destroy() } func test_addMetadata() throws { // given inserted metadata - let metadata = try storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) + let metadata = storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) XCTAssertNotNil(metadata) // then the record should exist in storage - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssert(try metadata!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0].key, "test") + XCTAssertEqual(records[0].type, .resource) + XCTAssertEqual(records[0].lifespan, .permanent) } func test_addMetadata_resourceLimit() throws { // given limit reached on resources for i in 1...storage.options.resourcesLimit { - try storage.addMetadata( + storage.addMetadata( key: "metadata_\(i)", value: "test", type: .resource, @@ -123,25 +43,20 @@ class MetadataRecordTests: XCTestCase { } // when inserting a new resource - let resource = try storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) + let resource = storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) // then it should not be inserted XCTAssertNil(resource) // then the record count should be the limit - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), storage.options.resourcesLimit) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, storage.options.resourcesLimit) } func test_addMetadata_customPropertiesLimit() throws { // given limit reached on custom properties for i in 1...storage.options.customPropertiesLimit { - try storage.addMetadata( + storage.addMetadata( key: "metadata_\(i)", value: "test", type: .customProperty, @@ -150,26 +65,21 @@ class MetadataRecordTests: XCTestCase { } // when inserting a new custom property - let resource = try storage.addMetadata(key: "test", value: "test", type: .customProperty, lifespan: .permanent) + let resource = storage.addMetadata(key: "test", value: "test", type: .customProperty, lifespan: .permanent) // then it should not be inserted XCTAssertNil(resource) // then the record count should be the limit - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), storage.options.customPropertiesLimit) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, storage.options.customPropertiesLimit) } func test_addMetadata_resourceLimit_lifespanId() throws { // given resources in storage that in total surpass the limit // but they correspond to different lifespan ids for i in 1...storage.options.resourcesLimit { - try storage.addMetadata( + storage.addMetadata( key: "metadata_\(i)", value: "test", type: .resource, @@ -177,7 +87,7 @@ class MetadataRecordTests: XCTestCase { lifespanId: i % 2 == 0 ? TestConstants.sessionId.toString : "test" ) - try storage.addMetadata( + storage.addMetadata( key: "metadata_\(i)", value: "test", type: .resource, @@ -187,15 +97,15 @@ class MetadataRecordTests: XCTestCase { } // when inserting new resources - let resource1 = try storage.addMetadata( - key: "test", + let resource1 = storage.addMetadata( + key: "test1", value: "test", type: .resource, lifespan: .session, lifespanId: TestConstants.sessionId.toString ) - let resource2 = try storage.addMetadata( - key: "test", + let resource2 = storage.addMetadata( + key: "test2", value: "test", type: .resource, lifespan: .process, @@ -207,22 +117,17 @@ class MetadataRecordTests: XCTestCase { XCTAssertNotNil(resource2) // then the record count should be the limit - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), storage.options.customPropertiesLimit * 2 + 2) - XCTAssert(try resource1!.exists(db)) - XCTAssert(try resource2!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, storage.options.resourcesLimit * 2 + 2) + XCTAssertNotNil(records.first(where: { $0.key == "test1" })) + XCTAssertNotNil(records.first(where: { $0.key == "test2" })) } func test_addMetadata_customPropertiesLimit_lifespanId() throws { // given custom properties in storage that in total surpass the limit // but they correspond to different lifespan ids for i in 1...storage.options.customPropertiesLimit { - try storage.addMetadata( + storage.addMetadata( key: "metadata_\(i)", value: "test", type: .customProperty, @@ -230,7 +135,7 @@ class MetadataRecordTests: XCTestCase { lifespanId: i % 2 == 0 ? TestConstants.sessionId.toString : "test" ) - try storage.addMetadata( + storage.addMetadata( key: "metadata_\(i)", value: "test", type: .customProperty, @@ -240,15 +145,15 @@ class MetadataRecordTests: XCTestCase { } // when inserting new custom properties - let property1 = try storage.addMetadata( - key: "test", + let property1 = storage.addMetadata( + key: "test1", value: "test", type: .customProperty, lifespan: .session, lifespanId: TestConstants.sessionId.toString ) - let property2 = try storage.addMetadata( - key: "test", + let property2 = storage.addMetadata( + key: "test2", value: "test", type: .customProperty, lifespan: .process, @@ -260,26 +165,21 @@ class MetadataRecordTests: XCTestCase { XCTAssertNotNil(property2) // then the record count should be the limit - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), storage.options.customPropertiesLimit * 2 + 2) - XCTAssert(try property1!.exists(db)) - XCTAssert(try property2!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, storage.options.customPropertiesLimit * 2 + 2) + XCTAssertNotNil(records.first(where: { $0.key == "test1" })) + XCTAssertNotNil(records.first(where: { $0.key == "test2" })) } func test_addMetadata_requiredResource() throws { // given limit reached on resources and custom properties for i in 0...storage.options.resourcesLimit { - try storage.addMetadata(key: "resource_\(i)", value: "test", type: .resource, lifespan: .permanent) - try storage.addMetadata(key: "property_\(i)", value: "test", type: .customProperty, lifespan: .permanent) + storage.addMetadata(key: "resource_\(i)", value: "test", type: .resource, lifespan: .permanent) + storage.addMetadata(key: "property_\(i)", value: "test", type: .customProperty, lifespan: .permanent) } // when inserting a new required resource - let requiredResource = try storage.addMetadata( + let requiredResource = storage.addMetadata( key: "test", value: "test", type: .requiredResource, @@ -289,62 +189,48 @@ class MetadataRecordTests: XCTestCase { // then it should be inserted despite the limits XCTAssertNotNil(requiredResource) - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual( - try MetadataRecord.fetchCount(db), - storage.options.resourcesLimit + storage.options.customPropertiesLimit + 1 - ) - XCTAssert(try requiredResource!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, storage.options.resourcesLimit + storage.options.customPropertiesLimit + 1) + XCTAssertNotNil(records.first(where: { $0.key == "test" })) } func test_updateMetadata() throws { // given inserted record - try storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) + storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) // when updating its value - try storage.updateMetadata(key: "test", value: "value", type: .resource, lifespan: .permanent, lifespanId: "") + storage.updateMetadata(key: "test", value: "value", type: .resource, lifespan: .permanent, lifespanId: "") // then record should exist in storage with the correct value - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), 1) - let record = try MetadataRecord.fetchOne(db) - XCTAssertEqual(record!.value, .string("value")) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0].value, "value") } func test_cleanMetadata() throws { // given inserted records - let metadata1 = try storage.addMetadata( + storage.addMetadata( key: "test1", value: "test", type: .resource, lifespan: .session, lifespanId: TestConstants.sessionId.toString ) - let metadata2 = try storage.addMetadata( + storage.addMetadata( key: "test2", value: "test", type: .resource, lifespan: .session, lifespanId: "test" ) - let metadata3 = try storage.addMetadata( + storage.addMetadata( key: "test3", value: "test", type: .resource, lifespan: .process, lifespanId: TestConstants.processId.hex ) - let metadata4 = try storage.addMetadata( + storage.addMetadata( key: "test4", value: "test", type: .resource, @@ -353,28 +239,23 @@ class MetadataRecordTests: XCTestCase { ) // when cleaning old metadata - try storage.cleanMetadata( + storage.cleanMetadata( currentSessionId: TestConstants.sessionId.toString, currentProcessId: TestConstants.processId.hex ) // then only the correct records should be removed - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), 2) - XCTAssert(try metadata1!.exists(db)) - XCTAssertFalse(try metadata2!.exists(db)) - XCTAssert(try metadata3!.exists(db)) - XCTAssertFalse(try metadata4!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, 2) + XCTAssertNotNil(records.first(where: { $0.key == "test1" })) + XCTAssertNil(records.first(where: { $0.key == "test2" })) + XCTAssertNotNil(records.first(where: { $0.key == "test3" })) + XCTAssertNil(records.first(where: { $0.key == "test4" })) } func test_removeMetadata() throws { // given inserted record - let metadata = try storage.addMetadata( + storage.addMetadata( key: "test", value: "test", type: .resource, @@ -383,49 +264,43 @@ class MetadataRecordTests: XCTestCase { ) // when removing it - try storage.removeMetadata(key: "test", type: .resource, lifespan: .session, lifespanId: "test") + storage.removeMetadata(key: "test", type: .resource, lifespan: .session, lifespanId: "test") // then record should not exist in storage - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), 0) - XCTAssertFalse(try metadata!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, 0) } func test_removeAllMetadata_severalLifespans() throws { // given inserted records - let metadata1 = try storage.addMetadata( + storage.addMetadata( key: "test1", value: "test", type: .resource, lifespan: .session, lifespanId: "test" ) - let metadata2 = try storage.addMetadata( + storage.addMetadata( key: "test2", value: "test", type: .resource, lifespan: .process, lifespanId: "test" ) - let metadata3 = try storage.addMetadata( + storage.addMetadata( key: "test3", value: "test", type: .resource, lifespan: .session, lifespanId: "test" ) - let metadata4 = try storage.addMetadata( + storage.addMetadata( key: "test4", value: "test", type: .resource, lifespan: .permanent ) - let required = try storage.addMetadata( + storage.addMetadata( key: "test5", value: "test", type: .requiredResource, @@ -434,48 +309,43 @@ class MetadataRecordTests: XCTestCase { ) // when removing all by type and lifespans - try storage.removeAllMetadata(type: .resource, lifespans: [.session, .process]) + storage.removeAllMetadata(type: .resource, lifespans: [.session, .process]) // then only the correct records should be removed - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), 2) - XCTAssertFalse(try metadata1!.exists(db)) - XCTAssertFalse(try metadata2!.exists(db)) - XCTAssertFalse(try metadata3!.exists(db)) - XCTAssert(try metadata4!.exists(db)) - XCTAssert(try required!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, 2) + XCTAssertNil(records.first(where: { $0.key == "test1" })) + XCTAssertNil(records.first(where: { $0.key == "test2" })) + XCTAssertNil(records.first(where: { $0.key == "test3" })) + XCTAssertNotNil(records.first(where: { $0.key == "test4" })) + XCTAssertNotNil(records.first(where: { $0.key == "test5" })) } func test_removeAllMetadata_severalKeys() throws { // given inserted records - let metadata1 = try storage.addMetadata( + storage.addMetadata( key: "test1", value: "test", type: .resource, lifespan: .process, lifespanId: "test" ) - let metadata2 = try storage.addMetadata( + storage.addMetadata( key: "test2", value: "test", type: .resource, lifespan: .process, lifespanId: "test" ) - let metadata3 = try storage.addMetadata( + storage.addMetadata( key: "test3", value: "test", type: .resource, lifespan: .process, lifespanId: "test" ) - let required = try storage.addMetadata( - key: "test5", + storage.addMetadata( + key: "test4", value: "test", type: .requiredResource, lifespan: .process, @@ -483,28 +353,23 @@ class MetadataRecordTests: XCTestCase { ) // when removing all by keys and lifespan - try storage.removeAllMetadata(keys: ["test1", "test3", "test5"], lifespan: .process) + storage.removeAllMetadata(keys: ["test1", "test3", "test4"], lifespan: .process) // then only the correct records should be removed - let expectation = XCTestExpectation() - try storage.dbQueue.read { db in - XCTAssertEqual(try MetadataRecord.fetchCount(db), 2) - XCTAssertFalse(try metadata1!.exists(db)) - XCTAssert(try metadata2!.exists(db)) - XCTAssertFalse(try metadata3!.exists(db)) - XCTAssert(try required!.exists(db)) - expectation.fulfill() - } - - wait(for: [expectation], timeout: .defaultTimeout) + let records: [MetadataRecord] = storage.fetchAll() + XCTAssertEqual(records.count, 2) + XCTAssertNil(records.first(where: { $0.key == "test1" })) + XCTAssertNotNil(records.first(where: { $0.key == "test2" })) + XCTAssertNil(records.first(where: { $0.key == "test3" })) + XCTAssertNotNil(records.first(where: { $0.key == "test4" })) } func test_fetchMetadata() throws { // given inserted record - try storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) + storage.addMetadata(key: "test", value: "test", type: .resource, lifespan: .permanent) // when fetching it - let record = try storage.fetchMetadata(key: "test", type: .resource, lifespan: .permanent) + let record = storage.fetchMetadata(key: "test", type: .resource, lifespan: .permanent) // then its correctly fetched XCTAssertNotNil(record) @@ -512,10 +377,10 @@ class MetadataRecordTests: XCTestCase { func test_fetchRequiredPermanentResource() throws { // given inserted permanent required resource - try storage.addMetadata(key: "test", value: "test", type: .requiredResource, lifespan: .permanent) + storage.addMetadata(key: "test", value: "test", type: .requiredResource, lifespan: .permanent) // when fetching it - let record = try storage.fetchRequiredPermanentResource(key: "test") + let record = storage.fetchRequiredPermanentResource(key: "test") // then its correctly fetched XCTAssertNotNil(record) @@ -523,42 +388,42 @@ class MetadataRecordTests: XCTestCase { func test_fetchAllResources() throws { // given inserted records - let resource1 = try storage.addMetadata( + storage.addMetadata( key: "test1", value: "test", type: .resource, lifespan: .process, lifespanId: "test" ) - let resource2 = try storage.addMetadata( + storage.addMetadata( key: "test2", value: "test", type: .requiredResource, lifespan: .process, lifespanId: "test" ) - let resource3 = try storage.addMetadata( + storage.addMetadata( key: "test3", value: "test", type: .resource, lifespan: .process, lifespanId: "test" ) - let property1 = try storage.addMetadata( + storage.addMetadata( key: "test4", value: "test", type: .customProperty, lifespan: .session, lifespanId: "test" ) - let property2 = try storage.addMetadata( + storage.addMetadata( key: "test5", value: "test", type: .customProperty, lifespan: .session, lifespanId: "test" ) - let property3 = try storage.addMetadata( + storage.addMetadata( key: "test6", value: "test", type: .customProperty, @@ -567,72 +432,72 @@ class MetadataRecordTests: XCTestCase { ) // when fetching all resources - let resources = try storage.fetchAllResources() + let resources = storage.fetchAllResources() // then the correct records are fetched XCTAssertEqual(resources.count, 3) - XCTAssert(resources.contains(resource1!)) - XCTAssert(resources.contains(resource2!)) - XCTAssert(resources.contains(resource3!)) - XCTAssertFalse(resources.contains(property1!)) - XCTAssertFalse(resources.contains(property2!)) - XCTAssertFalse(resources.contains(property3!)) + XCTAssertNotNil(resources.first(where: { $0.key == "test1" })) + XCTAssertNotNil(resources.first(where: { $0.key == "test2" })) + XCTAssertNotNil(resources.first(where: { $0.key == "test3" })) + XCTAssertNil(resources.first(where: { $0.key == "test4" })) + XCTAssertNil(resources.first(where: { $0.key == "test5" })) + XCTAssertNil(resources.first(where: { $0.key == "test6" })) } func test_fetchResourcesForSessionId() throws { // given a session in storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date() ) // given inserted records - let resource1 = try storage.addMetadata( + storage.addMetadata( key: "test1", value: "test", type: .resource, lifespan: .process, lifespanId: TestConstants.processId.hex ) - let resource2 = try storage.addMetadata( + storage.addMetadata( key: "test2", value: "test", type: .resource, lifespan: .process, lifespanId: "test" ) - let resource3 = try storage.addMetadata( + storage.addMetadata( key: "test3", value: "test", type: .resource, lifespan: .session, lifespanId: TestConstants.sessionId.toString ) - let resource4 = try storage.addMetadata( + storage.addMetadata( key: "test4", value: "test", type: .resource, lifespan: .session, lifespanId: "test" ) - let resource5 = try storage.addMetadata( + storage.addMetadata( key: "test5", value: "test", type: .resource, lifespan: .permanent ) - let property1 = try storage.addMetadata( + storage.addMetadata( key: "test6", value: "test", type: .customProperty, lifespan: .process, lifespanId: TestConstants.processId.hex ) - let property2 = try storage.addMetadata( + storage.addMetadata( key: "test7", value: "test", type: .customProperty, @@ -641,73 +506,73 @@ class MetadataRecordTests: XCTestCase { ) // when fetching all resources by session id - let resources = try storage.fetchResourcesForSessionId(TestConstants.sessionId) + let resources = storage.fetchResourcesForSessionId(TestConstants.sessionId) // then the correct records are fetched XCTAssertEqual(resources.count, 3) - XCTAssert(resources.contains(resource1!)) - XCTAssertFalse(resources.contains(resource2!)) - XCTAssert(resources.contains(resource3!)) - XCTAssertFalse(resources.contains(resource4!)) - XCTAssert(resources.contains(resource5!)) - XCTAssertFalse(resources.contains(property1!)) - XCTAssertFalse(resources.contains(property2!)) + XCTAssertNotNil(resources.first(where: { $0.key == "test1" })) + XCTAssertNil(resources.first(where: { $0.key == "test2" })) + XCTAssertNotNil(resources.first(where: { $0.key == "test3" })) + XCTAssertNil(resources.first(where: { $0.key == "test4" })) + XCTAssertNotNil(resources.first(where: { $0.key == "test5" })) + XCTAssertNil(resources.first(where: { $0.key == "test6" })) + XCTAssertNil(resources.first(where: { $0.key == "test7" })) } func test_fetchCustomPropertiesForSessionId() throws { // given a session in storage - try storage.addSession( + storage.addSession( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: Date() ) // given inserted records - let property1 = try storage.addMetadata( + storage.addMetadata( key: "test1", value: "test", type: .customProperty, lifespan: .process, lifespanId: TestConstants.processId.hex ) - let property2 = try storage.addMetadata( + storage.addMetadata( key: "test2", value: "test", type: .customProperty, lifespan: .process, lifespanId: "test" ) - let property3 = try storage.addMetadata( + storage.addMetadata( key: "test3", value: "test", type: .customProperty, lifespan: .session, lifespanId: TestConstants.sessionId.toString ) - let property4 = try storage.addMetadata( + storage.addMetadata( key: "test4", value: "test", type: .customProperty, lifespan: .session, lifespanId: "test" ) - let property5 = try storage.addMetadata( + storage.addMetadata( key: "test5", value: "test", type: .customProperty, lifespan: .permanent ) - let resource1 = try storage.addMetadata( + storage.addMetadata( key: "test6", value: "test", type: .resource, lifespan: .process, lifespanId: TestConstants.processId.hex ) - let resource2 = try storage.addMetadata( + storage.addMetadata( key: "test7", value: "test", type: .resource, @@ -716,16 +581,16 @@ class MetadataRecordTests: XCTestCase { ) // when fetching all resources by session id - let resources = try storage.fetchCustomPropertiesForSessionId(TestConstants.sessionId) + let resources = storage.fetchCustomPropertiesForSessionId(TestConstants.sessionId) // then the correct records are fetched XCTAssertEqual(resources.count, 3) - XCTAssert(resources.contains(property1!)) - XCTAssertFalse(resources.contains(property2!)) - XCTAssert(resources.contains(property3!)) - XCTAssertFalse(resources.contains(property4!)) - XCTAssert(resources.contains(property5!)) - XCTAssertFalse(resources.contains(resource1!)) - XCTAssertFalse(resources.contains(resource2!)) + XCTAssertNotNil(resources.first(where: { $0.key == "test1" })) + XCTAssertNil(resources.first(where: { $0.key == "test2" })) + XCTAssertNotNil(resources.first(where: { $0.key == "test3" })) + XCTAssertNil(resources.first(where: { $0.key == "test4" })) + XCTAssertNotNil(resources.first(where: { $0.key == "test5" })) + XCTAssertNil(resources.first(where: { $0.key == "test6" })) + XCTAssertNil(resources.first(where: { $0.key == "test7" })) } } diff --git a/Tests/EmbraceStorageInternalTests/Migration/MigrationServiceTests.swift b/Tests/EmbraceStorageInternalTests/Migration/MigrationServiceTests.swift deleted file mode 100644 index 60734ec1..00000000 --- a/Tests/EmbraceStorageInternalTests/Migration/MigrationServiceTests.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest -import TestSupport -import GRDB -@testable import EmbraceStorageInternal - -class MigrationServiceTests: XCTestCase { - var dbQueue: DatabaseQueue! - var migrationService = MigrationService(logger: MockLogger()) - - override func setUpWithError() throws { - dbQueue = try DatabaseQueue(named: name) - } - - func test_perform_runsMigrations_thatAreNotRun() throws { - // Given an existing table - try dbQueue.write { db in - try TestMigrationRecord.defineTable(db: db) - } - - // Checking it only contains the original schema - try dbQueue.read { db in - let columns = try db.columns(in: "test_migrations") - XCTAssertEqual(columns.count, 1) - XCTAssertEqual(columns[0].name, "id") - } - - // When performing a migration - let migrations: [Migration] = [ - AddColumnSomethingNew(), - AddColumnSomethingNewer() - ] - try migrationService.perform(dbQueue, migrations: migrations) - - // Then all migrations have been completed and all new keys have been added to the table. - try dbQueue.read { db in - /// Check database now has 3 columns. - let columns = try db.columns(in: "test_migrations") - XCTAssertEqual(columns.count, 3) - - /// Check all expected migrations have been completed - let identifiers = try DatabaseMigrator().appliedIdentifiers(db) - XCTAssertEqual( - Set(identifiers), - Set(["AddColumnSomethingNew_1", "AddColumnSomethingNewer_2"]) - ) - - /// Check new expected columns have been added. - let somethingNew = columns.first { column in - column.name == "something_new" - } - let somethingNewer = columns.first { column in - column.name == "something_newer" - } - XCTAssertNotNil(somethingNew) - XCTAssertNotNil(somethingNewer) - } - } - - func test_perform_whenTableIsDefined_andMigrationTriesToRedefineIt_doesNotFail() throws { - // Given an existing table - try dbQueue.write { db in - try TestMigrationRecord.defineTable(db: db) - } - - // When performing a migration - try migrationService.perform(dbQueue, migrations: [ - InitialSchema(), - AddColumnSomethingNew(), - AddColumnSomethingNewer() - ]) - - try dbQueue.read { db in - let columns = try db.columns(in: "test_migrations") - XCTAssertEqual(columns.count, 3) - - XCTAssertNotNil(columns.first { info in - info.name == "id" && - info.type == "TEXT" && - info.isNotNull - }) - - XCTAssertNotNil(columns.first { info in - info.name == "something_new" && - info.type == "TEXT" && - info.isNotNull == false - }) - - XCTAssertNotNil(columns.first { info in - info.name == "something_newer" && - info.type == "TEXT" && - info.isNotNull == false - }) - } - } - - func test_perform_whenRunMultipleTimes_doesNotFail() throws { - for _ in 0..<5 { - try migrationService.perform(dbQueue, migrations: [ - InitialSchema(), - AddColumnSomethingNew(), - AddColumnSomethingNewer() - ]) - } - - try dbQueue.read { db in - let columns = try db.columns(in: "test_migrations") - - XCTAssertEqual(columns.count, 3) - XCTAssertNotNil(columns.first { info in - info.name == "id" && - info.type == "TEXT" && - info.isNotNull - }) - - XCTAssertNotNil(columns.first { info in - info.name == "something_new" && - info.type == "TEXT" && - info.isNotNull == false - }) - - XCTAssertNotNil(columns.first { info in - info.name == "something_newer" && - info.type == "TEXT" && - info.isNotNull == false - }) - } - } -} - -extension MigrationServiceTests { - - struct TestMigrationRecord: TableRecord { - internal static func defineTable(db: Database) throws { - try db.create(table: "test_migrations", options: .ifNotExists) { t in - t.primaryKey("id", .text).notNull() - } - } - } - - class InitialSchema: Migration { - static var identifier = "Initial Schema" - - func perform(_ db: GRDB.Database) throws { - try TestMigrationRecord.defineTable(db: db) - } - } - - class AddColumnSomethingNew: Migration { - static let identifier = "AddColumnSomethingNew_1" - - func perform(_ db: Database) throws { - try db.alter(table: "test_migrations") { table in - table.add(column: "something_new", .text) - } - } - } - - class AddColumnSomethingNewer: Migration { - static let identifier: StringLiteralType = "AddColumnSomethingNewer_2" - - func perform(_ db: GRDB.Database) throws { - try db.alter(table: "test_migrations") { table in - table.add(column: "something_newer", .text) - } - } - } - -} diff --git a/Tests/EmbraceStorageInternalTests/Migration/MigrationTests.swift b/Tests/EmbraceStorageInternalTests/Migration/MigrationTests.swift deleted file mode 100644 index 19b6886b..00000000 --- a/Tests/EmbraceStorageInternalTests/Migration/MigrationTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest - -@testable import EmbraceStorageInternal -import GRDB - -final class MigrationTests: XCTestCase { - - func test_migration_hasDefault_foreignKeyChecks() throws { - let migration = ExampleMigration() - - XCTAssertEqual(migration.foreignKeyChecks, .immediate) - XCTAssertEqual(type(of: migration).foreignKeyChecks, .immediate) - } - - func test_migration_allowsForCustom_foreignKeyChecks() throws { - - let migration = CustomForeignKeyMigration() - - XCTAssertEqual(migration.foreignKeyChecks, .deferred) - XCTAssertEqual(type(of: migration).foreignKeyChecks, .deferred) - - XCTAssertEqual(migration.identifier, "CustomForeignKeyMigration_001") - XCTAssertEqual(type(of: migration).identifier, "CustomForeignKeyMigration_001") - } -} - -extension MigrationTests { - struct ExampleMigration: Migration { - static let identifier = "ExampleMigration_001" - func perform(_ db: GRDB.Database) throws { } - } - - struct CustomForeignKeyMigration: Migration { - static let foreignKeyChecks: DatabaseMigrator.ForeignKeyChecks = .deferred - - static let identifier = "CustomForeignKeyMigration_001" - func perform(_ db: GRDB.Database) throws { } - } -} diff --git a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240509_00_AddSpanRecordMigrationTests.swift b/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240509_00_AddSpanRecordMigrationTests.swift deleted file mode 100644 index d8ba98bc..00000000 --- a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240509_00_AddSpanRecordMigrationTests.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest - -@testable import EmbraceStorageInternal -import GRDB - -final class AddSpanRecordMigrationTests: XCTestCase { - - var storage: EmbraceStorage! - - override func setUpWithError() throws { - storage = try EmbraceStorage.createInMemoryDb(runMigrations: false) - } - - override func tearDownWithError() throws { - try storage.teardown() - } - - func test_identifier() { - let migration = AddSpanRecordMigration() - XCTAssertEqual(migration.identifier, "CreateSpanRecordTable") - } - - func test_perform_createsTableWithCorrectSchema() throws { - let migration = AddSpanRecordMigration() - - try storage.dbQueue.write { db in - try migration.perform(db) - } - - try storage.dbQueue.read { db in - XCTAssertTrue(try db.tableExists(SpanRecord.databaseTableName)) - - let columns = try db.columns(in: SpanRecord.databaseTableName) - XCTAssertEqual(columns.count, 7) - - // primary key - XCTAssert(try db.table( - SpanRecord.databaseTableName, - hasUniqueKey: [ - SpanRecord.Schema.traceId.name, - SpanRecord.Schema.id.name - ] - )) - - // id - let idColumn = columns.first { info in - info.name == SpanRecord.Schema.id.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(idColumn) - - // name - let nameColumn = columns.first { info in - info.name == SpanRecord.Schema.name.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(nameColumn) - - // trace_id - let traceIdColumn = columns.first { info in - info.name == SpanRecord.Schema.traceId.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(traceIdColumn) - - // type - let typeColumn = columns.first { info in - info.name == SpanRecord.Schema.type.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(typeColumn) - - // start_time - let startTimeColumn = columns.first { info in - info.name == SpanRecord.Schema.startTime.name && - info.type == "DATETIME" && - info.isNotNull == true - } - XCTAssertNotNil(startTimeColumn) - - // end_time - let endTimeColumn = columns.first { info in - info.name == SpanRecord.Schema.endTime.name && - info.type == "DATETIME" && - info.isNotNull == false - } - XCTAssertNotNil(endTimeColumn) - - // data - let dataColumn = columns.first { info in - info.name == SpanRecord.Schema.data.name && - info.type == "BLOB" && - info.isNotNull == true - } - XCTAssertNotNil(dataColumn) - } - } - - func test_perform_createsClosedSpanTrigger() throws { - let migration = AddSpanRecordMigration() - - try storage.dbQueue.write { db in - try migration.perform(db) - } - - try storage.dbQueue.read { db in - let rows = try Row.fetchAll(db, sql: "SELECT * FROM sqlite_master where type = 'trigger'") - XCTAssertEqual(rows.count, 1) - - let triggerRow = try XCTUnwrap(rows.first) - XCTAssertEqual(triggerRow["name"], "prevent_closed_span_modification") - XCTAssertEqual(triggerRow["tbl_name"], "spans") - } - } -} diff --git a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_00_AddSessionRecordMigrationTests.swift b/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_00_AddSessionRecordMigrationTests.swift deleted file mode 100644 index 8f24509d..00000000 --- a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_00_AddSessionRecordMigrationTests.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest - -@testable import EmbraceStorageInternal -import GRDB - -final class _0240510_AddSessionRecordMigrationTests: XCTestCase { - - var storage: EmbraceStorage! - - override func setUpWithError() throws { - storage = try EmbraceStorage.createInMemoryDb(runMigrations: false) - } - - override func tearDownWithError() throws { - try storage.teardown() - } - - func test_identifier() { - let migration = AddSessionRecordMigration() - XCTAssertEqual(migration.identifier, "CreateSessionRecordTable") - } - - func test_perform_createsTableWithCorrectSchema() throws { - let migration = AddSessionRecordMigration() - - try storage.dbQueue.write { db in - try migration.perform(db) - } - - try storage.dbQueue.read { db in - XCTAssert(try db.tableExists(SessionRecord.databaseTableName)) - - XCTAssert(try db.table(SessionRecord.databaseTableName, hasUniqueKey: [SessionRecord.Schema.id.name])) - - let columns = try db.columns(in: SessionRecord.databaseTableName) - XCTAssertEqual(columns.count, 12) - - // id - let idColumn = columns.first { info in - info.name == SessionRecord.Schema.id.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(idColumn) - - // state - let stateTimeColumn = columns.first { info in - info.name == SessionRecord.Schema.state.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(stateTimeColumn) - - // process_id - let processIdColumn = columns.first { info in - info.name == SessionRecord.Schema.processId.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(processIdColumn) - - // trace_id - let traceIdColumn = columns.first { info in - info.name == SessionRecord.Schema.traceId.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(traceIdColumn) - - // span_id - let spanIdColumn = columns.first { info in - info.name == SessionRecord.Schema.spanId.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(spanIdColumn) - - // start_time - let startTimeColumn = columns.first { info in - info.name == SessionRecord.Schema.startTime.name && - info.type == "DATETIME" && - info.isNotNull == true - } - XCTAssertNotNil(startTimeColumn) - - // end_time - let endTimeColumn = columns.first { info in - info.name == SessionRecord.Schema.endTime.name && - info.type == "DATETIME" && - info.isNotNull == false - } - XCTAssertNotNil(endTimeColumn) - - // last_heartbeat_time - let lastHeartbeatTimeColumn = columns.first { info in - info.name == SessionRecord.Schema.lastHeartbeatTime.name && - info.type == "DATETIME" && - info.isNotNull == true - } - XCTAssertNotNil(lastHeartbeatTimeColumn) - - // cold_start - let coldStartColumn = columns.first { info in - info.name == SessionRecord.Schema.coldStart.name && - info.type == "BOOLEAN" && - info.isNotNull == true && - info.defaultValueSQL == "0" - } - XCTAssertNotNil(coldStartColumn) - - // clean_exit - let cleanExitColumn = columns.first { info in - info.name == SessionRecord.Schema.cleanExit.name && - info.type == "BOOLEAN" && - info.isNotNull == true && - info.defaultValueSQL == "0" - } - XCTAssertNotNil(cleanExitColumn) - - // app_terminated - let appTerminatedColumn = columns.first { info in - info.name == SessionRecord.Schema.appTerminated.name && - info.type == "BOOLEAN" && - info.isNotNull == true && - info.defaultValueSQL == "0" - } - XCTAssertNotNil(appTerminatedColumn) - - // crash_report_id - let crashReportIdColumn = columns.first { info in - info.name == SessionRecord.Schema.crashReportId.name && - info.type == "TEXT" && - info.isNotNull == false - } - XCTAssertNotNil(crashReportIdColumn) - } - } - -} diff --git a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_01_AddMetadataRecordMigrationTests.swift b/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_01_AddMetadataRecordMigrationTests.swift deleted file mode 100644 index 8bc48d69..00000000 --- a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_01_AddMetadataRecordMigrationTests.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest - -@testable import EmbraceStorageInternal -import GRDB - -final class _0240510_01_AddMetadataRecordMigrationTests: XCTestCase { - - var storage: EmbraceStorage! - - override func setUpWithError() throws { - storage = try EmbraceStorage.createInMemoryDb(runMigrations: false) - } - - override func tearDownWithError() throws { - try storage.teardown() - } - - func test_identifier() { - let migration = AddMetadataRecordMigration() - XCTAssertEqual(migration.identifier, "CreateMetadataRecordTable") - } - - func test_perform_createsTableWithCorrectSchema() throws { - let migration = AddMetadataRecordMigration() - - try storage.dbQueue.write { db in - try migration.perform(db) - } - - try storage.dbQueue.read { db in - XCTAssertTrue(try db.tableExists(MetadataRecord.databaseTableName)) - - XCTAssertTrue( - try db.table(MetadataRecord.databaseTableName, - hasUniqueKey: [ - MetadataRecord.Schema.key.name, - MetadataRecord.Schema.type.name, - MetadataRecord.Schema.lifespan.name, - MetadataRecord.Schema.lifespanId.name - ] ) - ) - - let columns = try db.columns(in: MetadataRecord.databaseTableName) - XCTAssertEqual(columns.count, 6) - - // key - let keyColumn = columns.first { info in - info.name == MetadataRecord.Schema.key.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(keyColumn) - - // value - let valueColumn = columns.first { info in - info.name == MetadataRecord.Schema.value.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(valueColumn) - - // type - let typeColumn = columns.first { info in - info.name == MetadataRecord.Schema.type.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(typeColumn) - - // lifespan - let lifespanColumn = columns.first { info in - info.name == MetadataRecord.Schema.lifespan.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(lifespanColumn) - - // lifespan_id - let lifespanIdColumn = columns.first { info in - info.name == MetadataRecord.Schema.lifespanId.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(lifespanIdColumn) - - // collected_at - let collectedAtColumn = columns.first { info in - info.name == MetadataRecord.Schema.collectedAt.name && - info.type == "DATETIME" && - info.isNotNull == true - } - XCTAssertNotNil(collectedAtColumn) - } - } -} diff --git a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_02_AddLogRecordMigrationTests.swift b/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_02_AddLogRecordMigrationTests.swift deleted file mode 100644 index a0524242..00000000 --- a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240510_02_AddLogRecordMigrationTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest - -@testable import EmbraceStorageInternal -import GRDB - -final class _0240510_02_AddLogRecordMigrationTests: XCTestCase { - - var storage: EmbraceStorage! - - override func setUpWithError() throws { - storage = try EmbraceStorage.createInMemoryDb(runMigrations: false) - } - - override func tearDownWithError() throws { - try storage.teardown() - } - - func test_identifier() { - let migration = AddLogRecordMigration() - XCTAssertEqual(migration.identifier, "CreateLogRecordTable") - } - - func test_perform_createsTableWithCorrectSchema() throws { - let migration = AddLogRecordMigration() - - try storage.dbQueue.write { db in - try migration.perform(db) - } - - try storage.dbQueue.read { db in - XCTAssert(try db.tableExists(LogRecord.databaseTableName)) - - let columns = try db.columns(in: LogRecord.databaseTableName) - - XCTAssert(try db.table( - LogRecord.databaseTableName, - hasUniqueKey: ["identifier"] - )) - - // identifier - let idColumn = columns.first { info in - info.name == "identifier" && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(idColumn, "identifier column not found!") - - // process_identifier - let processIdColumn = columns.first { info in - info.name == "process_identifier" && - info.type == "INTEGER" && - info.isNotNull == true - } - XCTAssertNotNil(processIdColumn, "process_identifier column not found!") - - // severity - let severityColumn = columns.first { info in - info.name == "severity" && - info.type == "INTEGER" && - info.isNotNull == true - } - XCTAssertNotNil(severityColumn, "severity column not found!") - - // body - let bodyColumn = columns.first { info in - info.name == "body" && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(bodyColumn, "body column not found!") - - // timestamp - let timestampColumn = columns.first { info in - info.name == "timestamp" && - info.type == "DATETIME" && - info.isNotNull == true - } - XCTAssertNotNil(timestampColumn, "timestamp column not found!") - - // attributes - let attributesColumn = columns.first { info in - info.name == "attributes" && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(attributesColumn, "attributes column not found!") - } - } - -} diff --git a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigrationTests.swift b/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigrationTests.swift deleted file mode 100644 index 4bc731ae..00000000 --- a/Tests/EmbraceStorageInternalTests/Migration/Migrations/20240523_00_AddProcessIdentifierToSpanRecordMigrationTests.swift +++ /dev/null @@ -1,222 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import XCTest -@testable import EmbraceStorageInternal -import EmbraceOTelInternal -import EmbraceCommonInternal -import GRDB -import OpenTelemetryApi - -final class _0240523_00_AddProcessIdentifierToSpanRecordMigration: XCTestCase { - - var storage: EmbraceStorage! - - override func setUpWithError() throws { - storage = try EmbraceStorage.createInMemoryDb(runMigrations: false) - } - - override func tearDownWithError() throws { - try storage.teardown() - } - - func test_identifier() { - let migration = AddProcessIdentifierToSpanRecordMigration() - XCTAssertEqual(migration.identifier, "AddProcessIdentifierToSpanRecord") - } - - func test_perform_withNoExistingRecords() throws { - let migration = AddProcessIdentifierToSpanRecordMigration() - try storage.performMigration(migrations: .current.upTo(identifier: migration.identifier)) - - try storage.dbQueue.write { db in - try migration.perform(db) - } - - try storage.dbQueue.read { db in - XCTAssertTrue(try db.tableExists(SpanRecord.databaseTableName)) - - let columns = try db.columns(in: SpanRecord.databaseTableName) - XCTAssertEqual(columns.count, 8) - - // primary key - XCTAssert(try db.table( - SpanRecord.databaseTableName, - hasUniqueKey: [ - SpanRecord.Schema.traceId.name, - SpanRecord.Schema.id.name - ] - )) - - // id - let idColumn = columns.first { info in - info.name == SpanRecord.Schema.id.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(idColumn) - - // name - let nameColumn = columns.first { info in - info.name == SpanRecord.Schema.name.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(nameColumn) - - // trace_id - let traceIdColumn = columns.first { info in - info.name == SpanRecord.Schema.traceId.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(traceIdColumn) - - // type - let typeColumn = columns.first { info in - info.name == SpanRecord.Schema.type.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(typeColumn) - - // start_time - let startTimeColumn = columns.first { info in - info.name == SpanRecord.Schema.startTime.name && - info.type == "DATETIME" && - info.isNotNull == true - } - XCTAssertNotNil(startTimeColumn) - - // end_time - let endTimeColumn = columns.first { info in - info.name == SpanRecord.Schema.endTime.name && - info.type == "DATETIME" && - info.isNotNull == false - } - XCTAssertNotNil(endTimeColumn) - - // data - let dataColumn = columns.first { info in - info.name == SpanRecord.Schema.data.name && - info.type == "BLOB" && - info.isNotNull == true - } - XCTAssertNotNil(dataColumn) - - // process_identifier - let processIdentifier = columns.first { info in - info.name == SpanRecord.Schema.processIdentifier.name && - info.type == "TEXT" && - info.isNotNull == true - } - XCTAssertNotNil(processIdentifier) - } - } - - func test_perform_migratesExistingEntries() throws { - let migration = AddProcessIdentifierToSpanRecordMigration() - try storage.performMigration(migrations: .current.upTo(identifier: migration.identifier)) - - try storage.dbQueue.write { db in - try db.execute(sql: """ - INSERT INTO 'spans' ( - 'id', - 'trace_id', - 'name', - 'type', - 'start_time', - 'end_time', - 'data' - ) VALUES ( - ?, - ?, - ?, - ?, - ?, - ?, - ? - ); - """, arguments: [ - "3d9381a7f8300102", - "b65cd80e1bea6fd2c27150f8cce3de3e", - "example-name", - SpanType.performance, - Date(), - Date(timeIntervalSinceNow: 2), - Data() - ]) - } - - try storage.dbQueue.write { db in - try migration.perform(db) - } - - try storage.dbQueue.read { db in - let rows = try Row.fetchAll(db, sql: "SELECT * from spans") - XCTAssertEqual(rows.count, 1) - - let records = try SpanRecord.fetchAll(db) - XCTAssertEqual(records.count, 1) - records.forEach { record in - XCTAssertEqual(record.processIdentifier, ProcessIdentifier(hex: "c0ffee")) - } - } - } - - func test_perform_migratesExistingEntries_whenMultiple() throws { - let migration = AddProcessIdentifierToSpanRecordMigration() - try storage.performMigration(migrations: .current.upTo(identifier: migration.identifier)) - - let count = 10 - for _ in 0.. Self { - let idx = firstIndex { element in - type(of: element).identifier == identifier - } - - guard let idx = idx else { - return [] - } - - return Array(prefix(idx)) - } -} diff --git a/Tests/EmbraceStorageInternalTests/TestSupport/ThrowingMigration.swift b/Tests/EmbraceStorageInternalTests/TestSupport/ThrowingMigration.swift deleted file mode 100644 index 9632d535..00000000 --- a/Tests/EmbraceStorageInternalTests/TestSupport/ThrowingMigration.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceStorageInternal -import GRDB - -class ThrowingMigration: Migration { - enum MigrationServiceError: Error { - case alwaysError - } - - static var identifier = "AlwaysThrowingMigration" - - let performsToThrow: [Int] - private(set) var currentPerformCount: Int = 0 - - /// - Parameters: - /// - performToThrow: The invocation of `perform` that should throw. Defaults to 1, the first call to `perform`. - init(performsToThrow: [Int]) { - self.performsToThrow = performsToThrow - } - - /// - Parameters: - /// - performToThrow: The invocation of `perform` that should throw. Defaults to 1, the first call to `perform`. - convenience init(performToThrow: Int = 1) { - self.init(performsToThrow: [performToThrow]) - } - - func perform(_ db: GRDB.Database) throws { - currentPerformCount += 1 - if performsToThrow.contains(currentPerformCount) { - throw MigrationServiceError.alwaysError - } - } -} diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift index 723bb374..e36c715f 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift @@ -72,10 +72,10 @@ extension EmbraceUploadCacheTests { } // when attempting to remove data over the allowed days - let removedRecords = try cache.clearStaleDataIfNeeded() + let removedRecords = cache.clearStaleDataIfNeeded() // the expected records should've been removed. - let records = try cache.fetchAllUploadData() + let records = cache.fetchAllUploadData() XCTAssertEqual(removedRecords, 2) XCTAssert(!records.contains(record2)) XCTAssert(!records.contains(record3)) @@ -154,10 +154,10 @@ extension EmbraceUploadCacheTests { } // when attempting to remove data over the allowed days - let removedRecords = try cache.clearStaleDataIfNeeded() + let removedRecords = cache.clearStaleDataIfNeeded() // no records should've been removed - let records = try cache.fetchAllUploadData() + let records = cache.fetchAllUploadData() XCTAssertEqual(removedRecords, 0) XCTAssert(records.contains(record2)) XCTAssert(records.contains(record3)) @@ -173,7 +173,7 @@ extension EmbraceUploadCacheTests { let cache = try EmbraceUploadCache(options: options, logger: MockLogger()) // when attempting to remove data from an empty cache - let removedRecords = try cache.clearStaleDataIfNeeded() + let removedRecords = cache.clearStaleDataIfNeeded() // no records should've been removed XCTAssertEqual(removedRecords, 0) @@ -243,10 +243,10 @@ extension EmbraceUploadCacheTests { } // when attempting to remove data over the allowed days - let removedRecords = try cache.clearStaleDataIfNeeded() + let removedRecords = cache.clearStaleDataIfNeeded() // no records should've been removed - let records = try cache.fetchAllUploadData() + let records = cache.fetchAllUploadData() XCTAssertEqual(removedRecords, 0) XCTAssert(records.contains(record2)) XCTAssert(records.contains(record3)) diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift index 4d1cb02f..153d0946 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift @@ -78,7 +78,7 @@ class EmbraceUploadCacheTests: XCTestCase { cache.coreData.save() // when fetching the upload datas - let datas = try cache.fetchAllUploadData() + let datas = cache.fetchAllUploadData() // then the fetched datas are valid XCTAssert(datas.contains(data1)) diff --git a/Tests/TestSupport/Extensions/EmbraceStorage+Extension.swift b/Tests/TestSupport/Extensions/EmbraceStorage+Extension.swift index 8649db78..25db8441 100644 --- a/Tests/TestSupport/Extensions/EmbraceStorage+Extension.swift +++ b/Tests/TestSupport/Extensions/EmbraceStorage+Extension.swift @@ -3,29 +3,25 @@ // import Foundation +import EmbraceCommonInternal @testable import EmbraceStorageInternal public extension EmbraceStorage { static func createInMemoryDb(runMigrations: Bool = true) throws -> EmbraceStorage { - let storage = try EmbraceStorage(options: .init(named: UUID().uuidString), logger: MockLogger()) - if runMigrations { try storage.performMigration() } + let storage = try EmbraceStorage( + options: .init(storageMechanism: .inMemory(name: UUID().uuidString)), + logger: MockLogger() + ) return storage } - static func createInDiskDb(runMigrations: Bool = true) throws -> EmbraceStorage { + static func createInDiskDb(fileName: String) throws -> EmbraceStorage { let url = URL(fileURLWithPath: NSTemporaryDirectory()) let storage = try EmbraceStorage( - options: .init(baseUrl: url, fileName: "\(UUID().uuidString).sqlite"), + options: .init(storageMechanism: .onDisk(name: fileName, baseURL: url)), logger: MockLogger() ) - if runMigrations { try storage.performMigration() } return storage } } - -public extension EmbraceStorage { - func teardown() throws { - try dbQueue.close() - } -} From fcf289e2f6c242068fbd824a2af2f74c89ba2874 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Wed, 5 Feb 2025 13:32:44 -0300 Subject: [PATCH 12/17] WIP --- Package.swift | 6 +- .../Storage/Model/EmbraceLog.swift | 28 +++++++ .../Storage/Model/EmbraceLogAttribute.swift | 46 +++++++++++ .../Storage/Model/EmbraceMetadata.swift | 49 ++++++++++++ .../Storage/Model/EmbraceSession.swift | 30 +++++++ .../Storage/Model/EmbraceSpan.swift | 26 +++++++ .../EmbraceCore/Capture/CaptureServices.swift | 2 +- .../NetworkPayloadCaptureHandler.swift | 2 +- .../Internal/Logs/DefaultInternalLogger.swift | 4 +- .../Logs/EmbraceLogAttributesBuilder.swift | 6 +- .../Logs/Exporter/DefaultLogBatcher.swift | 14 ++-- .../Internal/Logs/Exporter/LogBatch.swift | 8 +- .../Internal/Logs/LogController.swift | 14 ++-- .../EmbraceCore/Payload/AppInfoPayload.swift | 2 +- .../Payload/Builders/LogPayloadBuilder.swift | 8 +- .../Builders/SessionPayloadBuilder.swift | 10 +-- .../Builders/SpansPayloadBuilder.swift | 22 +++--- .../EmbraceCore/Payload/MetadataPayload.swift | 4 +- .../EmbraceCore/Payload/ResourcePayload.swift | 2 +- .../Payload/Utils/PayloadUtils.swift | 4 +- .../Metadata/MetadataHandler+Personas.swift | 2 +- .../DataRecovery/UnsentDataHandler.swift | 14 ++-- .../Session/SessionControllable.swift | 4 +- .../Session/SessionController.swift | 11 ++- .../Session/SessionSpanUtils.swift | 8 +- .../CoreDataWrapper.swift | 10 ++- .../Records/EmbraceStorage+Log.swift | 15 ++-- .../Records/EmbraceStorage+Metadata.swift | 24 +++--- .../Records/EmbraceStorage+Span.swift | 12 +-- .../Records/LogAttributeRecord.swift | 36 +-------- .../Records/LogRecord.swift | 21 ++--- .../Records/MetadataRecord.swift | 35 +-------- .../Records/SessionRecord.swift | 12 +-- .../Records/SpanRecord.swift | 10 +-- .../NetworkPayloadCaptureHandlerTests.swift | 2 +- .../Internal/DefaultInternalLoggerTests.swift | 3 + .../EmbraceLogAttributesBuilderTests.swift | 16 ++-- .../StorageEmbraceLogExporterTests.swift | 2 +- .../Internal/Logs/LogControllerTests.swift | 19 +++-- .../Internal/Logs/LogsBatchTests.swift | 15 ++-- .../Payload/LogPayloadBuilderTests.swift | 24 +++--- .../Payload/MetadataPayloadTests.swift | 20 ++--- .../Payload/PayloadUtilTests.swift | 23 ++++-- .../Payload/ResourcePayloadTests.swift | 55 ++++++------- .../Payload/SessionPayloadBuilderTests.swift | 4 +- .../Payload/SpansPayloadBuilderTests.swift | 6 +- .../Session/SessionControllerTests.swift | 8 +- .../Session/SessionSpanUtilsTests.swift | 17 ++-- .../TestSupport/Records/LogRecord+Init.swift | 46 ----------- .../Records/MetadataRecord+Init.swift | 29 ------- .../TestDoubles/MockMetadataFetcher.swift | 16 ++-- .../TestDoubles/MockSessionController.swift | 10 +-- .../TestDoubles/SpyLogBatcherDelegate.swift | 5 +- .../TestDoubles/SpyLogRepository.swift | 11 +-- .../TestSupport/TestDoubles/SpyStorage.swift | 37 ++++----- .../Utilities/MetadataRecord+Factory.swift | 54 ------------- .../Utilities/SessionRecord+Factory.swift | 20 ----- .../EmbraceStorageLoggingTests.swift | 2 +- .../Mocks/DummyLogControllable.swift | 2 +- Tests/TestSupport/Mocks/Model/MockLog.swift | 68 ++++++++++++++++ .../Mocks/Model/MockMetadata.swift | 78 +++++++++++++++++++ .../Mocks/Model/MockSession.swift} | 35 +++++++-- 62 files changed, 640 insertions(+), 488 deletions(-) create mode 100644 Sources/EmbraceCommonInternal/Storage/Model/EmbraceLog.swift create mode 100644 Sources/EmbraceCommonInternal/Storage/Model/EmbraceLogAttribute.swift create mode 100644 Sources/EmbraceCommonInternal/Storage/Model/EmbraceMetadata.swift create mode 100644 Sources/EmbraceCommonInternal/Storage/Model/EmbraceSession.swift create mode 100644 Sources/EmbraceCommonInternal/Storage/Model/EmbraceSpan.swift delete mode 100644 Tests/EmbraceCoreTests/TestSupport/Records/LogRecord+Init.swift delete mode 100644 Tests/EmbraceCoreTests/TestSupport/Records/MetadataRecord+Init.swift delete mode 100644 Tests/EmbraceCoreTests/TestSupport/Utilities/MetadataRecord+Factory.swift delete mode 100644 Tests/EmbraceCoreTests/TestSupport/Utilities/SessionRecord+Factory.swift create mode 100644 Tests/TestSupport/Mocks/Model/MockLog.swift create mode 100644 Tests/TestSupport/Mocks/Model/MockMetadata.swift rename Tests/{EmbraceCoreTests/TestSupport/Records/SessionRecord+Init.swift => TestSupport/Mocks/Model/MockSession.swift} (52%) diff --git a/Package.swift b/Package.swift index cbd42c42..e80e224d 100644 --- a/Package.swift +++ b/Package.swift @@ -94,7 +94,10 @@ let package = Package( // common -------------------------------------------------------------------- .target( - name: "EmbraceCommonInternal" + name: "EmbraceCommonInternal", + dependencies: [ + .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift") + ] ), .testTarget( name: "EmbraceCommonInternalTests", @@ -183,6 +186,7 @@ let package = Package( name: "EmbraceStorageInternal", dependencies: [ "EmbraceCommonInternal", + "EmbraceCoreDataInternal", "EmbraceSemantics" ] ), diff --git a/Sources/EmbraceCommonInternal/Storage/Model/EmbraceLog.swift b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceLog.swift new file mode 100644 index 00000000..43f16caf --- /dev/null +++ b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceLog.swift @@ -0,0 +1,28 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import OpenTelemetryApi + +public protocol EmbraceLog { + var idRaw: String { get set } + var processIdRaw: String { get set } + var severityRaw: Int { get set } + var body: String { get set } + var timestamp: Date { get set } + + func allAttributes() -> [EmbraceLogAttribute] + func attribute(forKey key: String) -> EmbraceLogAttribute? + func setAttributeValue(value: AttributeValue, forKey key: String) +} + +public extension EmbraceLog { + var processId: ProcessIdentifier? { + return ProcessIdentifier(hex: processIdRaw) + } + + var severity: LogSeverity { + return LogSeverity(rawValue: severityRaw) ?? .info + } +} diff --git a/Sources/EmbraceCommonInternal/Storage/Model/EmbraceLogAttribute.swift b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceLogAttribute.swift new file mode 100644 index 00000000..8cadaa1e --- /dev/null +++ b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceLogAttribute.swift @@ -0,0 +1,46 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import OpenTelemetryApi + +public enum EmbraceLogAttributeType: Int { + case string, int, double, bool +} + +public protocol EmbraceLogAttribute { + var key: String { get set } + var valueRaw: String { get set } + var typeRaw: Int { get set } +} + +public extension EmbraceLogAttribute { + + var value: AttributeValue { + get { + let type = EmbraceLogAttributeType(rawValue: typeRaw) ?? .string + + switch type { + case .int: return AttributeValue(Int(valueRaw) ?? 0) + case .double: return AttributeValue(Double(valueRaw) ?? 0) + case .bool: return AttributeValue(Bool(valueRaw) ?? false) + default: return AttributeValue(valueRaw) + } + } + + set { + valueRaw = newValue.description + typeRaw = typeForValue(newValue).rawValue + } + } + + func typeForValue(_ value: AttributeValue) -> EmbraceLogAttributeType { + switch value { + case .int: return .int + case .double: return .double + case .bool: return .bool + default: return .string + } + } +} diff --git a/Sources/EmbraceCommonInternal/Storage/Model/EmbraceMetadata.swift b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceMetadata.swift new file mode 100644 index 00000000..9a483867 --- /dev/null +++ b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceMetadata.swift @@ -0,0 +1,49 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +public enum MetadataRecordType: String, Codable { + /// Resource that is attached to session and logs data + case resource + + /// Embrace-generated resource that is deemed required and cannot be removed by the user of the SDK + case requiredResource + + /// Custom property attached to session and logs data and that can be manipulated by the user of the SDK + case customProperty + + /// Persona tag attached to session and logs data and that can be manipulated by the user of the SDK + case personaTag +} + +public enum MetadataRecordLifespan: String, Codable { + /// Value tied to a specific session + case session + + /// Value tied to multiple sessions within a single process + case process + + /// Value tied to all sessions until explicitly removed + case permanent +} + +public protocol EmbraceMetadata { + var key: String { get set } + var value: String { get set } + var typeRaw: String { get set } + var lifespanRaw: String { get set } + var lifespanId: String { get set } + var collectedAt: Date { get set } +} + +public extension EmbraceMetadata { + var type: MetadataRecordType? { + return MetadataRecordType(rawValue: typeRaw) + } + + var lifespan: MetadataRecordLifespan? { + return MetadataRecordLifespan(rawValue: lifespanRaw) + } +} diff --git a/Sources/EmbraceCommonInternal/Storage/Model/EmbraceSession.swift b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceSession.swift new file mode 100644 index 00000000..c3c8e921 --- /dev/null +++ b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceSession.swift @@ -0,0 +1,30 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +public protocol EmbraceSession { + var idRaw: String { get set } + var processIdRaw: String { get set } + var state: String { get set } + var traceId: String { get set } + var spanId: String { get set } + var startTime: Date { get set } + var endTime: Date? { get set } + var lastHeartbeatTime: Date { get set } + var crashReportId: String? { get set } + var coldStart: Bool { get set } + var cleanExit: Bool { get set } + var appTerminated: Bool { get set } +} + +public extension EmbraceSession { + var id: SessionIdentifier? { + return SessionIdentifier(string: idRaw) + } + + var processId: ProcessIdentifier? { + return ProcessIdentifier(hex: processIdRaw) + } +} diff --git a/Sources/EmbraceCommonInternal/Storage/Model/EmbraceSpan.swift b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceSpan.swift new file mode 100644 index 00000000..c5b0d35d --- /dev/null +++ b/Sources/EmbraceCommonInternal/Storage/Model/EmbraceSpan.swift @@ -0,0 +1,26 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +public protocol EmbraceSpan { + var id: String { get set } + var name: String { get set } + var traceId: String { get set } + var typeRaw: String { get set } + var data: Data { get set } + var startTime: Date { get set } + var endTime: Date? { get set } + var processIdRaw: String { get set } +} + +public extension EmbraceSpan { + var type: SpanType? { + return SpanType(rawValue: typeRaw) + } + + var processId: ProcessIdentifier? { + return ProcessIdentifier(hex: processIdRaw) + } +} diff --git a/Sources/EmbraceCore/Capture/CaptureServices.swift b/Sources/EmbraceCore/Capture/CaptureServices.swift index dca696a0..b1fb3989 100644 --- a/Sources/EmbraceCore/Capture/CaptureServices.swift +++ b/Sources/EmbraceCore/Capture/CaptureServices.swift @@ -103,7 +103,7 @@ final class CaptureServices { } @objc func onSessionStart(notification: Notification) { - if let session = notification.object as? SessionRecord { + if let session = notification.object as? EmbraceSession { crashReporter?.currentSessionId = session.idRaw } } diff --git a/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandler.swift b/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandler.swift index b4bd5a21..af05f389 100644 --- a/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandler.swift +++ b/Sources/EmbraceCore/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandler.swift @@ -79,7 +79,7 @@ class NetworkPayloadCaptureHandler { active = true rulesTriggeredMap.removeAll() - currentSessionId = (notification.object as? SessionRecord)?.id + currentSessionId = (notification.object as? EmbraceSession)?.id } @objc func onSessionEnd() { diff --git a/Sources/EmbraceCore/Internal/Logs/DefaultInternalLogger.swift b/Sources/EmbraceCore/Internal/Logs/DefaultInternalLogger.swift index 6fce9670..6b3a9d5c 100644 --- a/Sources/EmbraceCore/Internal/Logs/DefaultInternalLogger.swift +++ b/Sources/EmbraceCore/Internal/Logs/DefaultInternalLogger.swift @@ -26,7 +26,7 @@ class DefaultInternalLogger: InternalLogger { private var counter: [LogLevel: Int] = [:] @ThreadSafe - private var currentSession: SessionRecord? + private var currentSession: EmbraceSession? init() { NotificationCenter.default.addObserver( @@ -49,7 +49,7 @@ class DefaultInternalLogger: InternalLogger { } @objc func onSessionStart(notification: Notification) { - currentSession = notification.object as? SessionRecord + currentSession = notification.object as? EmbraceSession counter.removeAll() } diff --git a/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift b/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift index 4e8c7172..b2ea852c 100644 --- a/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift +++ b/Sources/EmbraceCore/Internal/Logs/EmbraceLogAttributesBuilder.swift @@ -10,11 +10,11 @@ import EmbraceSemantics class EmbraceLogAttributesBuilder { private weak var storage: EmbraceStorageMetadataFetcher? private weak var sessionControllable: SessionControllable? - private var session: SessionRecord? + private var session: EmbraceSession? private var crashReport: CrashReport? private var attributes: [String: String] - private var currentSession: SessionRecord? { + private var currentSession: EmbraceSession? { session ?? sessionControllable?.currentSession } @@ -26,7 +26,7 @@ class EmbraceLogAttributesBuilder { self.attributes = initialAttributes } - init(session: SessionRecord?, + init(session: EmbraceSession?, crashReport: CrashReport? = nil, storage: EmbraceStorageMetadataFetcher? = nil, initialAttributes: [String: String]) { diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift index 47f0434c..19d42872 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift @@ -9,12 +9,12 @@ import OpenTelemetryApi import OpenTelemetrySdk protocol LogBatcherDelegate: AnyObject { - func batchFinished(withLogs logs: [LogRecord]) + func batchFinished(withLogs logs: [EmbraceLog]) } protocol LogBatcher { func addLogRecord(logRecord: ReadableLogRecord) - func renewBatch(withLogs logRecords: [LogRecord]) + func renewBatch(withLogs logRecords: [EmbraceLog]) func forceEndCurrentBatch() } @@ -61,23 +61,23 @@ internal extension DefaultLogBatcher { } } - func renewBatch(withLogs logRecords: [LogRecord] = []) { + func renewBatch(withLogs logs: [EmbraceLog] = []) { guard let batch = self.batch else { return } self.cancelBatchDeadline() self.delegate?.batchFinished(withLogs: batch.logs) - self.batch = .init(limits: self.logLimits, logs: logRecords) + self.batch = .init(limits: self.logLimits, logs: logs) - if logRecords.count > 0 { + if logs.count > 0 { self.renewBatchDeadline(with: self.logLimits) } } - func addLogToBatch(_ log: LogRecord) { + func addLogToBatch(_ log: EmbraceLog) { processorQueue.async { if let batch = self.batch { - let result = batch.add(logRecord: log) + let result = batch.add(log: log) switch result { case .success(let state): if state == .closed { diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/LogBatch.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/LogBatch.swift index 76f92ea1..1bfae3a0 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/LogBatch.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/LogBatch.swift @@ -18,7 +18,7 @@ struct LogsBatch { } @ThreadSafe - private(set) var logs: [LogRecord] + private(set) var logs: [EmbraceLog] private let limits: LogBatchLimits private var creationDate: Date? { @@ -36,16 +36,16 @@ struct LogsBatch { return .open } - init(limits: LogBatchLimits, logs: [LogRecord] = []) { + init(limits: LogBatchLimits, logs: [EmbraceLog] = []) { self.logs = logs self.limits = limits } - func add(logRecord: LogRecord) -> BatchingResult { + func add(log: EmbraceLog) -> BatchingResult { guard batchState == .open else { return .failure } - logs.append(logRecord) + logs.append(log) return .success(batchState: batchState) } } diff --git a/Sources/EmbraceCore/Internal/Logs/LogController.swift b/Sources/EmbraceCore/Internal/Logs/LogController.swift index 1558da27..509ad4fa 100644 --- a/Sources/EmbraceCore/Internal/Logs/LogController.swift +++ b/Sources/EmbraceCore/Internal/Logs/LogController.swift @@ -55,7 +55,7 @@ class LogController: LogControllable { return } - let logs: [LogRecord] = storage.fetchAll(excludingProcessIdentifier: .current) + let logs: [EmbraceLog] = storage.fetchAll(excludingProcessIdentifier: .current) if logs.count > 0 { send(batches: divideInBatches(logs)) } @@ -143,7 +143,7 @@ class LogController: LogControllable { } extension LogController { - func batchFinished(withLogs logs: [LogRecord]) { + func batchFinished(withLogs logs: [EmbraceLog]) { guard sdkStateProvider?.isEnabled == true else { return } @@ -210,7 +210,7 @@ private extension LogController { } func send( - logs: [LogRecord], + logs: [EmbraceLog], resourcePayload: ResourcePayload, metadataPayload: MetadataPayload ) { @@ -239,11 +239,11 @@ private extension LogController { } } - func divideInBatches(_ logs: [LogRecord]) -> [LogsBatch] { + func divideInBatches(_ logs: [EmbraceLog]) -> [LogsBatch] { var batches: [LogsBatch] = [] var batch: LogsBatch = .init(limits: .init(maxBatchAge: .infinity, maxLogsPerBatch: Self.maxLogsPerBatch)) for log in logs { - let result = batch.add(logRecord: log) + let result = batch.add(log: log) switch result { case .success(let batchState): if batchState == .closed { @@ -270,7 +270,7 @@ private extension LogController { throw Error.couldntAccessStorageModule } - var resources: [MetadataRecord] = [] + var resources: [EmbraceMetadata] = [] if let sessionId = sessionId { resources = storage.fetchResourcesForSessionId(sessionId) @@ -288,7 +288,7 @@ private extension LogController { throw Error.couldntAccessStorageModule } - var metadata: [MetadataRecord] = [] + var metadata: [EmbraceMetadata] = [] if let sessionId = sessionId { let properties = storage.fetchCustomPropertiesForSessionId(sessionId) diff --git a/Sources/EmbraceCore/Payload/AppInfoPayload.swift b/Sources/EmbraceCore/Payload/AppInfoPayload.swift index 14ee4fe7..34c4ac9d 100644 --- a/Sources/EmbraceCore/Payload/AppInfoPayload.swift +++ b/Sources/EmbraceCore/Payload/AppInfoPayload.swift @@ -30,7 +30,7 @@ struct AppInfoPayload: Codable { case appBundleId = "bid" } - init (with resources: [MetadataRecord]) { + init (with resources: [EmbraceMetadata]) { self.appBundleId = Bundle.main.bundleIdentifier resources.forEach { resource in diff --git a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift index 21c3733d..c663a96e 100644 --- a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift @@ -8,8 +8,8 @@ import EmbraceCommonInternal import EmbraceSemantics struct LogPayloadBuilder { - static func build(log: LogRecord) -> LogPayload { - var finalAttributes: [Attribute] = log.attributes.map { entry in + static func build(log: EmbraceLog) -> LogPayload { + var finalAttributes: [Attribute] = log.allAttributes().map { entry in Attribute(key: entry.key, value: entry.valueRaw) } @@ -32,8 +32,8 @@ struct LogPayloadBuilder { ) -> PayloadEnvelope<[LogPayload]> { // build resources and metadata payloads - var resources: [MetadataRecord] = [] - var metadata: [MetadataRecord] = [] + var resources: [EmbraceMetadata] = [] + var metadata: [EmbraceMetadata] = [] if let storage = storage { if let sessionId = sessionId { diff --git a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift index 76c20ba1..68d95f5e 100644 --- a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift @@ -10,8 +10,8 @@ class SessionPayloadBuilder { static var resourceName = "emb.session.upload_index" - class func build(for sessionRecord: SessionRecord, storage: EmbraceStorage) -> PayloadEnvelope<[SpanPayload]>? { - guard let sessionId = sessionRecord.id else { + class func build(for session: EmbraceSession, storage: EmbraceStorage) -> PayloadEnvelope<[SpanPayload]>? { + guard let sessionId = session.id else { return nil } @@ -35,17 +35,17 @@ class SessionPayloadBuilder { // build spans let (spans, spanSnapshots) = SpansPayloadBuilder.build( - for: sessionRecord, + for: session, storage: storage, sessionNumber: counter ) // build resources payload - let resources: [MetadataRecord] = storage.fetchResourcesForSessionId(sessionId) + let resources: [EmbraceMetadata] = storage.fetchResourcesForSessionId(sessionId) let resourcePayload = ResourcePayload(from: resources) // build metadata payload - var metadata: [MetadataRecord] = [] + var metadata: [EmbraceMetadata] = [] let properties = storage.fetchCustomPropertiesForSessionId(sessionId) let tags = storage.fetchPersonaTagsForSessionId(sessionId) metadata.append(contentsOf: properties) diff --git a/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift b/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift index 8c5c8f7a..d06a963a 100644 --- a/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift @@ -13,16 +13,16 @@ class SpansPayloadBuilder { static let spanCountLimit = 1000 class func build( - for sessionRecord: SessionRecord, + for session: EmbraceSession, storage: EmbraceStorage, sessionNumber: Int = -1 ) -> (spans: [SpanPayload], spanSnapshots: [SpanPayload]) { - let endTime = sessionRecord.endTime ?? sessionRecord.lastHeartbeatTime + let endTime = session.endTime ?? session.lastHeartbeatTime // fetch spans that started during the session // ignore spans where emb.type == session - let records = storage.fetchSpans(for: sessionRecord, ignoreSessionSpans: true, limit: spanCountLimit) + let records = storage.fetchSpans(for: session, ignoreSessionSpans: true, limit: spanCountLimit) // decode spans and separate them by closed/open var spans: [SpanPayload] = [] @@ -30,7 +30,7 @@ class SpansPayloadBuilder { // fetch and add session span first if let sessionSpanPayload = buildSessionSpanPayload( - for: sessionRecord, + for: session, storage: storage, sessionNumber: sessionNumber ) { @@ -45,7 +45,7 @@ class SpansPayloadBuilder { /// during the recovery process in `UnsentDataHandler`. /// In other words it was an open span at the time the app crashed, and thus it must be closed and flagged as failed. /// The nil check is just a sanity check to cover all bases. - let failed = sessionRecord.crashReportId != nil && (record.endTime == nil || record.endTime == endTime) + let failed = session.crashReportId != nil && (record.endTime == nil || record.endTime == endTime) let span = try JSONDecoder().decode(SpanData.self, from: record.data) let payload = SpanPayload(from: span, endTime: failed ? endTime : record.endTime, failed: failed) @@ -64,32 +64,32 @@ class SpansPayloadBuilder { } class func buildSessionSpanPayload( - for sessionRecord: SessionRecord, + for session: EmbraceSession, storage: EmbraceStorage, sessionNumber: Int ) -> SpanPayload? { do { var spanData: SpanData? - let sessionSpan = storage.fetchSpan(id: sessionRecord.spanId, traceId: sessionRecord.traceId) + let sessionSpan = storage.fetchSpan(id: session.spanId, traceId: session.traceId) if let rawData = sessionSpan?.data { spanData = try JSONDecoder().decode(SpanData.self, from: rawData) } - var properties: [MetadataRecord] = [] - if let sessionId = sessionRecord.id { + var properties: [EmbraceMetadata] = [] + if let sessionId = session.id { properties = storage.fetchCustomPropertiesForSessionId(sessionId) } return SessionSpanUtils.payload( - from: sessionRecord, + from: session, spanData: spanData, properties: properties, sessionNumber: sessionNumber ) } catch { - Embrace.logger.warning("Error fetching span for session \(sessionRecord.idRaw):\n\(error.localizedDescription)") + Embrace.logger.warning("Error fetching span for session \(session.idRaw):\n\(error.localizedDescription)") } return nil diff --git a/Sources/EmbraceCore/Payload/MetadataPayload.swift b/Sources/EmbraceCore/Payload/MetadataPayload.swift index 26d29667..a6203435 100644 --- a/Sources/EmbraceCore/Payload/MetadataPayload.swift +++ b/Sources/EmbraceCore/Payload/MetadataPayload.swift @@ -3,7 +3,7 @@ // import Foundation -import EmbraceStorageInternal +import EmbraceCommonInternal struct MetadataPayload: Codable { var locale: String? @@ -19,7 +19,7 @@ struct MetadataPayload: Codable { case userId = "user_id" } - init(from metadata: [MetadataRecord]) { + init(from metadata: [EmbraceMetadata]) { metadata.forEach { record in if let key = UserResourceKey(rawValue: record.key) { switch key { diff --git a/Sources/EmbraceCore/Payload/ResourcePayload.swift b/Sources/EmbraceCore/Payload/ResourcePayload.swift index c2b5c94c..4d6ae867 100644 --- a/Sources/EmbraceCore/Payload/ResourcePayload.swift +++ b/Sources/EmbraceCore/Payload/ResourcePayload.swift @@ -103,7 +103,7 @@ struct ResourcePayload: Codable { } } - init(from resources: [MetadataRecord]) { + init(from resources: [EmbraceMetadata]) { // bundle_id is constant and won't change over app install lifetime self.appBundleId = Bundle.main.bundleIdentifier diff --git a/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift b/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift index 823ead8a..c2a29c68 100644 --- a/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift +++ b/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift @@ -10,7 +10,7 @@ class PayloadUtils { static func fetchResources( from fetcher: EmbraceStorageMetadataFetcher, sessionId: SessionIdentifier? - ) -> [MetadataRecord] { + ) -> [EmbraceMetadata] { guard let sessionId = sessionId else { return [] @@ -22,7 +22,7 @@ class PayloadUtils { static func fetchCustomProperties( from fetcher: EmbraceStorageMetadataFetcher, sessionId: SessionIdentifier? - ) -> [MetadataRecord] { + ) -> [EmbraceMetadata] { guard let sessionId = sessionId else { return [] diff --git a/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift b/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift index d283aabc..00dc1805 100644 --- a/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift +++ b/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift @@ -14,7 +14,7 @@ extension MetadataHandler { return [] } - var records: [MetadataRecord] = [] + var records: [EmbraceMetadata] = [] if let sessionId = sessionController?.currentSession?.id { records = storage.fetchPersonaTagsForSessionId(sessionId) } else { diff --git a/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift b/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift index ad088f72..c927db1d 100644 --- a/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift +++ b/Sources/EmbraceCore/Session/DataRecovery/UnsentDataHandler.swift @@ -58,11 +58,11 @@ class UnsentDataHandler { for report in crashReports { // link session with crash report if possible - var session: SessionRecord? + var session: EmbraceSession? if let sessionId = SessionIdentifier(string: report.sessionId) { session = storage.fetchSession(id: sessionId) - if let session = session { + if var session = session { // update session's end time with the crash report timestamp session.endTime = report.timestamp ?? session.endTime @@ -99,7 +99,7 @@ class UnsentDataHandler { static public func sendCrashLog( report: CrashReport, reporter: CrashReporter?, - session: SessionRecord?, + session: EmbraceSession?, storage: EmbraceStorage?, upload: EmbraceUpload?, otel: EmbraceOpenTelemetry? @@ -154,7 +154,7 @@ class UnsentDataHandler { otel: EmbraceOpenTelemetry?, storage: EmbraceStorage?, report: CrashReport, - session: SessionRecord?, + session: EmbraceSession?, timestamp: Date ) -> [String: String] { @@ -213,7 +213,7 @@ class UnsentDataHandler { } static public func sendSession( - _ session: SessionRecord, + _ session: EmbraceSession, storage: EmbraceStorage, upload: EmbraceUpload, performCleanUp: Bool = true @@ -239,7 +239,9 @@ class UnsentDataHandler { case .success: // remove session from storage // we can remove this immediately because the upload module will cache it until the upload succeeds - storage.delete(session) + if let record = session as? SessionRecord { + storage.delete(record) + } if performCleanUp { cleanOldSpans(storage: storage) diff --git a/Sources/EmbraceCore/Session/SessionControllable.swift b/Sources/EmbraceCore/Session/SessionControllable.swift index f3404cc3..99fac767 100644 --- a/Sources/EmbraceCore/Session/SessionControllable.swift +++ b/Sources/EmbraceCore/Session/SessionControllable.swift @@ -10,10 +10,10 @@ import EmbraceStorageInternal /// See ``SessionController`` for main conformance protocol SessionControllable: AnyObject { - var currentSession: SessionRecord? { get } + var currentSession: EmbraceSession? { get } @discardableResult - func startSession(state: SessionState) -> SessionRecord? + func startSession(state: SessionState) -> EmbraceSession? @discardableResult func endSession() -> Date diff --git a/Sources/EmbraceCore/Session/SessionController.swift b/Sources/EmbraceCore/Session/SessionController.swift index 026f5884..0df65ced 100644 --- a/Sources/EmbraceCore/Session/SessionController.swift +++ b/Sources/EmbraceCore/Session/SessionController.swift @@ -26,7 +26,7 @@ public extension Notification.Name { class SessionController: SessionControllable { @ThreadSafe - private(set) var currentSession: SessionRecord? + private(set) var currentSession: EmbraceSession? @ThreadSafe private(set) var currentSessionSpan: Span? @@ -81,12 +81,12 @@ class SessionController: SessionControllable { } @discardableResult - func startSession(state: SessionState) -> SessionRecord? { + func startSession(state: SessionState) -> EmbraceSession? { return startSession(state: state, startTime: Date()) } @discardableResult - func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord? { + func startSession(state: SessionState, startTime: Date = Date()) -> EmbraceSession? { // end current session first if currentSession != nil { endSession() @@ -251,7 +251,10 @@ extension SessionController { return } - storage?.delete(session) + if let record = session as? SessionRecord { + storage?.delete(record) + } + currentSession = nil currentSessionSpan = nil } diff --git a/Sources/EmbraceCore/Session/SessionSpanUtils.swift b/Sources/EmbraceCore/Session/SessionSpanUtils.swift index 45fefd7e..33bf24bc 100644 --- a/Sources/EmbraceCore/Session/SessionSpanUtils.swift +++ b/Sources/EmbraceCore/Session/SessionSpanUtils.swift @@ -38,9 +38,9 @@ struct SessionSpanUtils { } static func payload( - from session: SessionRecord, + from session: EmbraceSession, spanData: SpanData? = nil, - properties: [MetadataRecord] = [], + properties: [EmbraceMetadata] = [], sessionNumber: Int ) -> SpanPayload { return SpanPayload(from: session, spanData: spanData, properties: properties, sessionNumber: sessionNumber) @@ -49,9 +49,9 @@ struct SessionSpanUtils { fileprivate extension SpanPayload { init( - from session: SessionRecord, + from session: EmbraceSession, spanData: SpanData? = nil, - properties: [MetadataRecord], + properties: [EmbraceMetadata], sessionNumber: Int ) { self.traceId = session.traceId diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index 59e0d3a3..db2012a5 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -10,8 +10,8 @@ public class CoreDataWrapper { public let options: CoreDataWrapper.Options - let container: NSPersistentContainer - public let context: NSManagedObjectContext + var container: NSPersistentContainer! + public private(set) var context: NSManagedObjectContext! let logger: InternalLogger @@ -52,7 +52,9 @@ public class CoreDataWrapper { } /// Removes the database file + /// - Note: Only used in tests!!! public func destroy() { +#if canImport(XCTest) context.reset() switch options.storageMechanism { @@ -76,6 +78,10 @@ public class CoreDataWrapper { logger.error("Error removing CoreData store!:\n\(error.localizedDescription)") } } + + container = nil + context = nil +#endif } /// Asynchronously saves all changes on the current context to disk diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift index a1233d68..b41be52c 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift @@ -14,9 +14,9 @@ public protocol LogRepository { body: String, timestamp: Date, attributes: [String: AttributeValue] - ) -> LogRecord - func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [LogRecord] - func remove(logs: [LogRecord]) + ) -> EmbraceLog + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] + func remove(logs: [EmbraceLog]) func removeAllLogs() } @@ -30,7 +30,7 @@ extension EmbraceStorage { body: String, timestamp: Date = Date(), attributes: [String: OpenTelemetryApi.AttributeValue] - ) -> LogRecord { + ) -> EmbraceLog { return LogRecord.create( context: coreData.context, id: id, @@ -42,7 +42,7 @@ extension EmbraceStorage { ) } - public func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [LogRecord] { + public func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] { let request = LogRecord.createFetchRequest() request.predicate = NSPredicate(format: "processIdRaw != %@", processIdentifier.hex) @@ -54,7 +54,8 @@ extension EmbraceStorage { remove(logs: logs) } - public func remove(logs: [LogRecord]) { - coreData.deleteRecords(logs) + public func remove(logs: [EmbraceLog]) { + let records = logs.compactMap( { $0 as? LogRecord } ) + coreData.deleteRecords(records) } } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift index 2d93e065..667b28cb 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift @@ -6,12 +6,12 @@ import Foundation import EmbraceCommonInternal public protocol EmbraceStorageMetadataFetcher: AnyObject { - func fetchAllResources() -> [MetadataRecord] - func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] - func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] - func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] - func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] - func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] + func fetchAllResources() -> [EmbraceMetadata] + func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] + func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] + func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] + func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] + func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] } extension EmbraceStorage { @@ -195,7 +195,7 @@ extension EmbraceStorage { } /// Returns all records with types `.requiredResource` or `.resource` - public func fetchAllResources() -> [MetadataRecord] { + public func fetchAllResources() -> [EmbraceMetadata] { let request = MetadataRecord.createFetchRequest() request.predicate = NSPredicate( format: "typeRaw == %@ OR typeRaw == %@", @@ -207,7 +207,7 @@ extension EmbraceStorage { } /// Returns all records with types `.requiredResource` or `.resource` that are tied to a given session id - public func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + public func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { guard let session = fetchSession(id: sessionId) else { return [] @@ -226,7 +226,7 @@ extension EmbraceStorage { } /// Returns all records with types `.requiredResource` or `.resource` that are tied to a given process id - public func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { + public func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( @@ -241,7 +241,7 @@ extension EmbraceStorage { } /// Returns all records of the `.customProperty` type that are tied to a given session id - public func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + public func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { guard let session = fetchSession(id: sessionId) else { return [] } @@ -259,7 +259,7 @@ extension EmbraceStorage { } /// Returns all records of the `.personaTag` type that are tied to a given session id - public func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + public func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { guard let session = fetchSession(id: sessionId) else { return [] } @@ -277,7 +277,7 @@ extension EmbraceStorage { } /// Returns all records of the `.personaTag` type that are tied to a given process id - public func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { + public func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { let request = MetadataRecord.createFetchRequest() request.predicate = NSCompoundPredicate( diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index 05950856..9927d6ce 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -116,10 +116,10 @@ extension EmbraceStorage { /// Will retrieve all spans that overlap with session record start / end (or last heartbeat) /// that occur within the same process. For cold start sessions, will include spans that occur before the session starts. /// Parameters: - /// - sessionRecord: The session record to fetch spans for + /// - session: The session record to fetch spans for /// - ignoreSessionSpans: Whether to ignore the session's (or any other session's) own span public func fetchSpans( - for sessionRecord: SessionRecord, + for session: EmbraceSession, ignoreSessionSpans: Bool = true, limit: Int = 1000 ) -> [SpanRecord] { @@ -127,21 +127,21 @@ extension EmbraceStorage { let request = SpanRecord.createFetchRequest() request.fetchLimit = limit - let endTime = (sessionRecord.endTime ?? sessionRecord.lastHeartbeatTime) as NSDate + let endTime = (session.endTime ?? session.lastHeartbeatTime) as NSDate // special case for cold start sessions // we grab spans that might have started before the session but within the same process - if sessionRecord.coldStart { + if session.coldStart { request.predicate = NSPredicate( format: "processIdRaw == %@ AND startTime <= %@", - sessionRecord.processIdRaw, + session.processIdRaw, endTime ) } // otherwise we check if the span is within the boundaries of the session else { - let startTime = sessionRecord.startTime as NSDate + let startTime = session.startTime as NSDate // span starts within session and // - ends before session ends or diff --git a/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift index be986152..0a23394a 100644 --- a/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift @@ -4,58 +4,28 @@ import Foundation import CoreData +import EmbraceCommonInternal import OpenTelemetryApi -public enum LogAttributeType: Int { - case string, int, double, bool -} - -public class LogAttributeRecord: NSManagedObject { +public class LogAttributeRecord: NSManagedObject, EmbraceLogAttribute { @NSManaged public var key: String @NSManaged public var valueRaw: String @NSManaged public var typeRaw: Int // LogAttributeType @NSManaged public var log: LogRecord - public var value: AttributeValue { - get { - let type = LogAttributeType(rawValue: typeRaw) ?? .string - - switch type { - case .int: return AttributeValue(Int(valueRaw) ?? 0) - case .double: return AttributeValue(Double(valueRaw) ?? 0) - case .bool: return AttributeValue(Bool(valueRaw) ?? false) - default: return AttributeValue(valueRaw) - } - } - - set { - valueRaw = newValue.description - typeRaw = Self.typeForValue(newValue).rawValue - } - } - public static func create( context: NSManagedObjectContext, key: String, value: AttributeValue, log: LogRecord ) -> LogAttributeRecord { - let record = LogAttributeRecord(context: context) + var record = LogAttributeRecord(context: context) record.key = key record.value = value record.log = log return record } - - private static func typeForValue(_ value: AttributeValue) -> LogAttributeType { - switch value { - case .int: return .int - case .double: return .double - case .bool: return .bool - default: return .string - } - } } extension LogAttributeRecord: EmbraceStorageRecord { diff --git a/Sources/EmbraceStorageInternal/Records/LogRecord.swift b/Sources/EmbraceStorageInternal/Records/LogRecord.swift index 6220b35d..0f7d8a0e 100644 --- a/Sources/EmbraceStorageInternal/Records/LogRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/LogRecord.swift @@ -7,7 +7,7 @@ import EmbraceCommonInternal import OpenTelemetryApi import CoreData -public class LogRecord: NSManagedObject { +public class LogRecord: NSManagedObject, EmbraceLog { @NSManaged public var idRaw: String // LogIdentifier @NSManaged public var processIdRaw: String // ProcessIdentifier @NSManaged public var severityRaw: Int // LogSeverity @@ -15,14 +15,6 @@ public class LogRecord: NSManagedObject { @NSManaged public var timestamp: Date @NSManaged public var attributes: [LogAttributeRecord] - public var processId: ProcessIdentifier? { - return ProcessIdentifier(hex: processIdRaw) - } - - public var severity: LogSeverity { - return LogSeverity(rawValue: severityRaw) ?? .info - } - static func create( context: NSManagedObjectContext, id: LogIdentifier, @@ -55,16 +47,19 @@ public class LogRecord: NSManagedObject { static func createFetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: entityName) } -} -extension LogRecord { - public func attribute(forKey key: String) -> LogAttributeRecord? { + public func allAttributes() -> [any EmbraceLogAttribute] { + return attributes + } + + public func attribute(forKey key: String) -> EmbraceLogAttribute? { return attributes.first(where: { $0.key == key }) } public func setAttributeValue(value: AttributeValue, forKey key: String) { - if let attribute = attribute(forKey: key) { + if var attribute = attribute(forKey: key) { attribute.value = value + return } guard let context = managedObjectContext else { diff --git a/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift b/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift index dfc44fca..c0b49ad8 100644 --- a/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift @@ -7,32 +7,7 @@ import EmbraceCommonInternal import CoreData import OpenTelemetryApi -public enum MetadataRecordType: String, Codable { - /// Resource that is attached to session and logs data - case resource - - /// Embrace-generated resource that is deemed required and cannot be removed by the user of the SDK - case requiredResource - - /// Custom property attached to session and logs data and that can be manipulated by the user of the SDK - case customProperty - - /// Persona tag attached to session and logs data and that can be manipulated by the user of the SDK - case personaTag -} - -public enum MetadataRecordLifespan: String, Codable { - /// Value tied to a specific session - case session - - /// Value tied to multiple sessions within a single process - case process - - /// Value tied to all sessions until explicitly removed - case permanent -} - -public class MetadataRecord: NSManagedObject { +public class MetadataRecord: NSManagedObject, EmbraceMetadata { @NSManaged public var key: String @NSManaged public var value: String @NSManaged public var typeRaw: String // MetadataRecordType @@ -40,14 +15,6 @@ public class MetadataRecord: NSManagedObject { @NSManaged public var lifespanId: String @NSManaged public var collectedAt: Date - public var type: MetadataRecordType? { - return MetadataRecordType(rawValue: typeRaw) - } - - public var lifespan: MetadataRecordLifespan? { - return MetadataRecordLifespan(rawValue: lifespanRaw) - } - public static func create( context: NSManagedObjectContext, key: String, diff --git a/Sources/EmbraceStorageInternal/Records/SessionRecord.swift b/Sources/EmbraceStorageInternal/Records/SessionRecord.swift index b72931c8..30700839 100644 --- a/Sources/EmbraceStorageInternal/Records/SessionRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/SessionRecord.swift @@ -7,7 +7,7 @@ import EmbraceCommonInternal import CoreData /// Represents a session in the storage -public class SessionRecord: NSManagedObject { +public class SessionRecord: NSManagedObject, EmbraceSession { @NSManaged public var idRaw: String // SessionIdentifier @NSManaged public var processIdRaw: String // ProcessIdentifier @NSManaged public var state: String @@ -27,14 +27,6 @@ public class SessionRecord: NSManagedObject { /// Used to mark the session that is active when the application was explicitly terminated by the user and/or system @NSManaged public var appTerminated: Bool - public var id: SessionIdentifier? { - return SessionIdentifier(string: idRaw) - } - - public var processId: ProcessIdentifier? { - return ProcessIdentifier(hex: processIdRaw) - } - public static func create( context: NSManagedObjectContext, id: SessionIdentifier, @@ -73,7 +65,7 @@ public class SessionRecord: NSManagedObject { } extension SessionRecord: EmbraceStorageRecord { - public static var entityName = "SessionRecord" + public static var entityName = "Session" static public var entityDescription: NSEntityDescription { let entity = NSEntityDescription() diff --git a/Sources/EmbraceStorageInternal/Records/SpanRecord.swift b/Sources/EmbraceStorageInternal/Records/SpanRecord.swift index 752d1844..0d0d77f1 100644 --- a/Sources/EmbraceStorageInternal/Records/SpanRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/SpanRecord.swift @@ -7,7 +7,7 @@ import EmbraceCommonInternal import CoreData /// Represents a span in the storage -public class SpanRecord: NSManagedObject { +public class SpanRecord: NSManagedObject, EmbraceSpan { @NSManaged public var id: String @NSManaged public var name: String @NSManaged public var traceId: String @@ -17,14 +17,6 @@ public class SpanRecord: NSManagedObject { @NSManaged public var endTime: Date? @NSManaged public var processIdRaw: String // ProcessIdentifier - public var type: SpanType? { - return SpanType(rawValue: typeRaw) - } - - public var processId: ProcessIdentifier? { - return ProcessIdentifier(hex: processIdRaw) - } - class func create( context: NSManagedObjectContext, id: String, diff --git a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift index cecb453d..eecd3c42 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift @@ -53,7 +53,7 @@ class NetworkPayloadCaptureHandlerTests: XCTestCase { handler.currentSessionId = nil // when a session starts - let session = SessionRecord( + let session = MockSession( id: TestConstants.sessionId, processId: TestConstants.processId, state: .foreground, diff --git a/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift b/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift index 9365377d..85fc96bf 100644 --- a/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift @@ -30,9 +30,11 @@ class DefaultInternalLoggerTests: XCTestCase { override func tearDownWithError() throws { storage.coreData.destroy() + storage = nil } func test_none() { + let logger = DefaultInternalLogger() logger.level = .none @@ -43,6 +45,7 @@ class DefaultInternalLoggerTests: XCTestCase { XCTAssertFalse(logger.error("error")) } + func test_trace() { let logger = DefaultInternalLogger() logger.level = .trace diff --git a/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift index a1300b5e..c12c4ab0 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift @@ -5,7 +5,7 @@ import XCTest import EmbraceStorageInternal import EmbraceCommonInternal - +import TestSupport @testable import EmbraceCore class EmbraceLogAttributesBuilderTests: XCTestCase { @@ -55,10 +55,10 @@ class EmbraceLogAttributesBuilderTests: XCTestCase { let sessionId = SessionIdentifier.random givenSessionController(sessionWithId: sessionId) givenMetadataFetcher(with: [ - .createSessionPropertyRecord(key: "custom_prop_int", value: .int(1), sessionId: sessionId), - .createSessionPropertyRecord(key: "custom_prop_bool", value: .bool(false), sessionId: sessionId), - .createSessionPropertyRecord(key: "custom_prop_double", value: .double(3.0), sessionId: sessionId), - .createSessionPropertyRecord(key: "custom_prop_string", value: .string("hello"), sessionId: sessionId)] + MockMetadata.createSessionPropertyRecord(key: "custom_prop_int", value: .int(1), sessionId: sessionId), + MockMetadata.createSessionPropertyRecord(key: "custom_prop_bool", value: .bool(false), sessionId: sessionId), + MockMetadata.createSessionPropertyRecord(key: "custom_prop_double", value: .double(3.0), sessionId: sessionId), + MockMetadata.createSessionPropertyRecord(key: "custom_prop_string", value: .string("hello"), sessionId: sessionId)] ) givenEmbraceLogAttributesBuilder() @@ -88,7 +88,7 @@ class EmbraceLogAttributesBuilderTests: XCTestCase { givenSessionControllerWithNoSession() // Shouldnt happen to have custom session properties with no session, but just in case :) givenMetadataFetcher(with: [ - .createSessionPropertyRecord(key: "custom_prop_string", value: .string("hello")) + MockMetadata.createSessionPropertyRecord(key: "custom_prop_string", value: .string("hello")) ]) givenEmbraceLogAttributesBuilder() @@ -178,14 +178,14 @@ private extension EmbraceLogAttributesBuilderTests { sessionState: SessionState = .foreground ) { controller = MockSessionController() - controller.currentSession = .with(id: sessionId, state: sessionState) + controller.currentSession = MockSession.with(id: sessionId, state: sessionState) } func givenSessionControllerWithNoSession() { controller = MockSessionController() } - func givenMetadataFetcher(with metadata: [MetadataRecord]? = nil) { + func givenMetadataFetcher(with metadata: [EmbraceMetadata]? = nil) { storage = .init(metadata: metadata ?? []) } diff --git a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift index 6e304f9f..91bf50b6 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/StorageEmbraceLogExporterTests.swift @@ -203,7 +203,7 @@ class SpyLogBatcher: LogBatcher { private(set) var didCallRenewBatch: Bool = false private(set) var renewBatchInvocationCount: Int = 0 - func renewBatch(withLogs logRecords: [LogRecord]) { + func renewBatch(withLogs logs: [EmbraceLog]) { didCallRenewBatch = true renewBatchInvocationCount += 1 } diff --git a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift index 0bd767ae..ae0bd145 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift @@ -296,7 +296,7 @@ private extension LogControllerTests { func givenSessionControllerWithSession() { sessionController = .init() - sessionController.currentSession = .init( + sessionController.currentSession = MockSession( id: .random, processId: .random, state: .foreground, @@ -306,7 +306,7 @@ private extension LogControllerTests { ) } - func givenStorage(withLogs logs: [LogRecord] = []) { + func givenStorage(withLogs logs: [EmbraceLog] = []) { storage = .init() storage?.stubbedFetchAllExcludingProcessIdentifier = logs } @@ -319,7 +319,7 @@ private extension LogControllerTests { sut.uploadAllPersistedLogs() } - func whenInvokingBatchFinished(withLogs logs: [LogRecord]) { + func whenInvokingBatchFinished(withLogs logs: [EmbraceLog]) { sut.batchFinished(withLogs: logs) } @@ -375,10 +375,13 @@ private extension LogControllerTests { XCTAssertTrue(upload.didCallUploadLog) } - func thenStorageShouldCallRemove(withLogs logs: [LogRecord]) throws { + func thenStorageShouldCallRemove(withLogs logs: [EmbraceLog]) throws { let unwrappedStorage = try XCTUnwrap(storage) wait(timeout: 1.0) { - unwrappedStorage.didCallRemoveLogs && unwrappedStorage.removeLogsReceivedParameter == logs + let expectedIds = logs.map { $0.idRaw } + let ids = unwrappedStorage.removeLogsReceivedParameter.map { $0.idRaw } + + return unwrappedStorage.didCallRemoveLogs && expectedIds == ids } } @@ -456,14 +459,14 @@ private extension LogControllerTests { } } - func randomLogRecord(sessionId: SessionIdentifier? = nil) -> LogRecord { + func randomLogRecord(sessionId: SessionIdentifier? = nil) -> EmbraceLog { var attributes: [String: AttributeValue] = [:] if let sessionId = sessionId { attributes["session.id"] = AttributeValue(sessionId.toString) } - return LogRecord( + return MockLog( id: .random, processId: .random, severity: .info, @@ -472,7 +475,7 @@ private extension LogControllerTests { ) } - func logsForMoreThanASingleBatch() -> [LogRecord] { + func logsForMoreThanASingleBatch() -> [EmbraceLog] { return (1...LogController.maxLogsPerBatch + 1).map { _ in randomLogRecord() } diff --git a/Tests/EmbraceCoreTests/Internal/Logs/LogsBatchTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/LogsBatchTests.swift index ff197a57..e805f491 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/LogsBatchTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/LogsBatchTests.swift @@ -4,7 +4,7 @@ import Foundation import XCTest - +import TestSupport import EmbraceCommonInternal import EmbraceStorageInternal @@ -76,7 +76,7 @@ private extension LogsBatchTests { limits = .init(maxBatchAge: maxBatchAge, maxLogsPerBatch: maxLogsPerBatch) } - func givenLogBatch(logs: [LogRecord] = []) { + func givenLogBatch(logs: [EmbraceLog] = []) { sut = .init(limits: limits, logs: logs) } @@ -84,8 +84,8 @@ private extension LogsBatchTests { givenLogBatch(logs: []) } - func whenAddingLog(_ log: LogRecord) { - batchingResult = sut.add(logRecord: log) + func whenAddingLog(_ log: EmbraceLog) { + batchingResult = sut.add(log: log) } func thenResult(is result: LogsBatch.BatchingResult) { @@ -100,12 +100,12 @@ private extension LogsBatchTests { XCTAssertEqual(sut.batchState, state) } - func recentLog() -> LogRecord { + func recentLog() -> EmbraceLog { randomLog(date: Date()) } - func randomLog(date: Date = Date()) -> LogRecord { - let recentLog = LogRecord( + func randomLog(date: Date = Date()) -> EmbraceLog { + return MockLog( id: .init(), processId: .random, severity: .info, @@ -113,7 +113,6 @@ private extension LogsBatchTests { timestamp: date, attributes: [:] ) - return recentLog } } diff --git a/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift b/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift index 17892c2a..133c567f 100644 --- a/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift @@ -12,11 +12,13 @@ import OpenTelemetryApi class LogPayloadBuilderTests: XCTestCase { func test_build_addsLogIdAttribute() throws { let logId = LogIdentifier(value: try XCTUnwrap(UUID(uuidString: "53B55EDD-889A-4876-86BA-6798288B609C"))) - let record = LogRecord(id: logId, - processId: .random, - severity: .info, - body: "Hello World", - attributes: .empty()) + let record = MockLog( + id: logId, + processId: .random, + severity: .info, + body: "Hello World", + attributes: .empty() + ) let payload = LogPayloadBuilder.build(log: record) @@ -32,11 +34,13 @@ class LogPayloadBuilderTests: XCTestCase { "boolean_attribute": .bool(false), "double_attribute": .double(5.0) ] - let record = LogRecord(id: .random, - processId: .random, - severity: .info, - body: .random(), - attributes: originalAttributes) + let record = MockLog( + id: .random, + processId: .random, + severity: .info, + body: .random(), + attributes: originalAttributes + ) let payload = LogPayloadBuilder.build(log: record) diff --git a/Tests/EmbraceCoreTests/Payload/MetadataPayloadTests.swift b/Tests/EmbraceCoreTests/Payload/MetadataPayloadTests.swift index 36d7a7f4..4aa81cd8 100644 --- a/Tests/EmbraceCoreTests/Payload/MetadataPayloadTests.swift +++ b/Tests/EmbraceCoreTests/Payload/MetadataPayloadTests.swift @@ -4,28 +4,28 @@ import XCTest import EmbraceStorageInternal - +import TestSupport @testable import EmbraceCore class MetadataPayloadTests: XCTestCase { func test_encodeToJSONProperly() throws { let payloadStruct = MetadataPayload(from: [ // wser Resources - MetadataRecord.userMetadata(key: UserResourceKey.email.rawValue, value: "email@domain.com"), - MetadataRecord.userMetadata(key: UserResourceKey.identifier.rawValue, value: "12345"), - MetadataRecord.userMetadata(key: UserResourceKey.name.rawValue, value: "embrace_user"), + MockMetadata.createUserMetadata(key: UserResourceKey.email.rawValue, value: "email@domain.com"), + MockMetadata.createUserMetadata(key: UserResourceKey.identifier.rawValue, value: "12345"), + MockMetadata.createUserMetadata(key: UserResourceKey.name.rawValue, value: "embrace_user"), // device Resources - MetadataRecord.createResourceRecord(key: DeviceResourceKey.locale.rawValue, value: "en_US_POSIX"), - MetadataRecord.createResourceRecord(key: DeviceResourceKey.timezone.rawValue, value: "GMT-3:00"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.locale.rawValue, value: "en_US_POSIX"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.timezone.rawValue, value: "GMT-3:00"), // random properties - MetadataRecord.userMetadata(key: "random_user_metadata_property", value: "value"), - MetadataRecord.createResourceRecord(key: "random_resource_property", value: "value"), + MockMetadata.createUserMetadata(key: "random_user_metadata_property", value: "value"), + MockMetadata.createResourceRecord(key: "random_resource_property", value: "value"), // persona tags - MetadataRecord.createPersonaTagRecord(value: "tag1"), - MetadataRecord.createPersonaTagRecord(value: "tag2") + MockMetadata.createPersonaTagRecord(value: "tag1"), + MockMetadata.createPersonaTagRecord(value: "tag2") ]) let jsonData = try JSONEncoder().encode(payloadStruct) diff --git a/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift b/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift index 0c15bb99..25db215b 100644 --- a/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift +++ b/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift @@ -7,12 +7,13 @@ import XCTest @testable import EmbraceStorageInternal @testable import EmbraceCommonInternal import OpenTelemetryApi +import TestSupport final class PayloadUtilTests: XCTestCase { func test_fetchResources() throws { // given - let mockResources: [MetadataRecord] = [ - MetadataRecord( + let mockResources: [EmbraceMetadata] = [ + MockMetadata( key: "fake_res", value: "fake_value", type: .requiredResource, @@ -26,7 +27,12 @@ final class PayloadUtilTests: XCTestCase { let fetchedResources = PayloadUtils.fetchResources(from: fetcher, sessionId: .random) // then the session payload contains the necessary keys - XCTAssertEqual(mockResources, fetchedResources) + XCTAssertEqual(fetchedResources.count, 1) + XCTAssertEqual(fetchedResources[0].key, mockResources[0].key) + XCTAssertEqual(fetchedResources[0].value, mockResources[0].value) + XCTAssertEqual(fetchedResources[0].type, mockResources[0].type) + XCTAssertEqual(fetchedResources[0].lifespan, mockResources[0].lifespan) + XCTAssertEqual(fetchedResources[0].lifespanId, mockResources[0].lifespanId) } func test_convertSpanAttributes() throws { @@ -65,8 +71,8 @@ final class PayloadUtilTests: XCTestCase { func test_fetchCustomProperties() throws { // given let sessionId = SessionIdentifier.random - let mockResources: [MetadataRecord] = [ - .init( + let mockResources: [EmbraceMetadata] = [ + MockMetadata( key: "fake_res", value: "fake_value", type: .customProperty, @@ -80,6 +86,11 @@ final class PayloadUtilTests: XCTestCase { let fetchedResources = PayloadUtils.fetchCustomProperties(from: fetcher, sessionId: sessionId) // then the session payload contains the necessary keys - XCTAssertEqual(mockResources, fetchedResources) + XCTAssertEqual(fetchedResources.count, 1) + XCTAssertEqual(fetchedResources[0].key, mockResources[0].key) + XCTAssertEqual(fetchedResources[0].value, mockResources[0].value) + XCTAssertEqual(fetchedResources[0].type, mockResources[0].type) + XCTAssertEqual(fetchedResources[0].lifespan, mockResources[0].lifespan) + XCTAssertEqual(fetchedResources[0].lifespanId, mockResources[0].lifespanId) } } diff --git a/Tests/EmbraceCoreTests/Payload/ResourcePayloadTests.swift b/Tests/EmbraceCoreTests/Payload/ResourcePayloadTests.swift index f0772400..e494f0d8 100644 --- a/Tests/EmbraceCoreTests/Payload/ResourcePayloadTests.swift +++ b/Tests/EmbraceCoreTests/Payload/ResourcePayloadTests.swift @@ -6,44 +6,45 @@ import XCTest import EmbraceStorageInternal import OpenTelemetrySdk @testable import EmbraceCore +import TestSupport class ResourcePayloadTests: XCTestCase { func test_encodeToJSONProperly() throws { let payloadStruct = ResourcePayload(from: [ // App Resources that should be present - MetadataRecord.userMetadata(key: AppResourceKey.bundleVersion.rawValue, value: "9.8.7"), - MetadataRecord.userMetadata(key: AppResourceKey.environment.rawValue, value: "dev"), - MetadataRecord.userMetadata(key: AppResourceKey.detailedEnvironment.rawValue, value: "si"), - MetadataRecord.userMetadata(key: AppResourceKey.framework.rawValue, value: "111"), - MetadataRecord.userMetadata(key: AppResourceKey.launchCount.rawValue, value: "123"), - MetadataRecord.userMetadata(key: AppResourceKey.appVersion.rawValue, value: "1.2.3"), - MetadataRecord.userMetadata(key: AppResourceKey.sdkVersion.rawValue, value: "3.2.1"), - MetadataRecord.userMetadata(key: AppResourceKey.processIdentifier.rawValue, value: "12345"), - MetadataRecord.userMetadata(key: AppResourceKey.buildID.rawValue, value: "fakebuilduuidnohyphen"), - MetadataRecord.userMetadata(key: AppResourceKey.processStartTime.rawValue, value: "12345"), - MetadataRecord.userMetadata(key: AppResourceKey.processPreWarm.rawValue, value: "true"), + MockMetadata.createUserMetadata(key: AppResourceKey.bundleVersion.rawValue, value: "9.8.7"), + MockMetadata.createUserMetadata(key: AppResourceKey.environment.rawValue, value: "dev"), + MockMetadata.createUserMetadata(key: AppResourceKey.detailedEnvironment.rawValue, value: "si"), + MockMetadata.createUserMetadata(key: AppResourceKey.framework.rawValue, value: "111"), + MockMetadata.createUserMetadata(key: AppResourceKey.launchCount.rawValue, value: "123"), + MockMetadata.createUserMetadata(key: AppResourceKey.appVersion.rawValue, value: "1.2.3"), + MockMetadata.createUserMetadata(key: AppResourceKey.sdkVersion.rawValue, value: "3.2.1"), + MockMetadata.createUserMetadata(key: AppResourceKey.processIdentifier.rawValue, value: "12345"), + MockMetadata.createUserMetadata(key: AppResourceKey.buildID.rawValue, value: "fakebuilduuidnohyphen"), + MockMetadata.createUserMetadata(key: AppResourceKey.processStartTime.rawValue, value: "12345"), + MockMetadata.createUserMetadata(key: AppResourceKey.processPreWarm.rawValue, value: "true"), // Device Resources that should be present - MetadataRecord.createResourceRecord(key: DeviceResourceKey.isJailbroken.rawValue, value: "true"), - MetadataRecord.createResourceRecord(key: DeviceResourceKey.totalDiskSpace.rawValue, value: "494384795648"), - MetadataRecord.createResourceRecord(key: DeviceResourceKey.architecture.rawValue, value: "arm64"), - MetadataRecord.createResourceRecord(key: ResourceAttributes.deviceModelIdentifier.rawValue, value: "arm64_model"), - MetadataRecord.createResourceRecord(key: ResourceAttributes.deviceManufacturer.rawValue, value: "Apple"), - MetadataRecord.createResourceRecord(key: DeviceResourceKey.screenResolution.rawValue, value: "1179x2556"), - MetadataRecord.createResourceRecord(key: ResourceAttributes.osVersion.rawValue, value: "17.0.1"), - MetadataRecord.createResourceRecord(key: DeviceResourceKey.osBuild.rawValue, value: "23D60"), - MetadataRecord.createResourceRecord(key: ResourceAttributes.osType.rawValue, value: "darwin"), - MetadataRecord.createResourceRecord(key: DeviceResourceKey.osVariant.rawValue, value: "iOS_variant"), - MetadataRecord.createResourceRecord(key: ResourceAttributes.osName.rawValue, value: "iPadOS"), - MetadataRecord.createResourceRecord(key: DeviceResourceKey.locale.rawValue, value: "en_US_POSIX"), - MetadataRecord.createResourceRecord(key: DeviceResourceKey.timezone.rawValue, value: "GMT-3:00"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.isJailbroken.rawValue, value: "true"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.totalDiskSpace.rawValue, value: "494384795648"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.architecture.rawValue, value: "arm64"), + MockMetadata.createResourceRecord(key: ResourceAttributes.deviceModelIdentifier.rawValue, value: "arm64_model"), + MockMetadata.createResourceRecord(key: ResourceAttributes.deviceManufacturer.rawValue, value: "Apple"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.screenResolution.rawValue, value: "1179x2556"), + MockMetadata.createResourceRecord(key: ResourceAttributes.osVersion.rawValue, value: "17.0.1"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.osBuild.rawValue, value: "23D60"), + MockMetadata.createResourceRecord(key: ResourceAttributes.osType.rawValue, value: "darwin"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.osVariant.rawValue, value: "iOS_variant"), + MockMetadata.createResourceRecord(key: ResourceAttributes.osName.rawValue, value: "iPadOS"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.locale.rawValue, value: "en_US_POSIX"), + MockMetadata.createResourceRecord(key: DeviceResourceKey.timezone.rawValue, value: "GMT-3:00"), // session counter - MetadataRecord.createResourceRecord(key: SessionPayloadBuilder.resourceName, value: "10"), + MockMetadata.createResourceRecord(key: SessionPayloadBuilder.resourceName, value: "10"), // Random properties that should be used - MetadataRecord.userMetadata(key: "random_user_metadata_property", value: "value1"), - MetadataRecord.createResourceRecord(key: "random_resource_property", value: "value2") + MockMetadata.createUserMetadata(key: "random_user_metadata_property", value: "value1"), + MockMetadata.createResourceRecord(key: "random_resource_property", value: "value2") ]) let jsonData = try JSONEncoder().encode(payloadStruct) diff --git a/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift b/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift index 1ddcafb5..e636e0dd 100644 --- a/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift @@ -11,12 +11,12 @@ import TestSupport final class SessionPayloadBuilderTests: XCTestCase { var storage: EmbraceStorage! - var sessionRecord: SessionRecord! + var sessionRecord: MockSession! override func setUpWithError() throws { storage = try EmbraceStorage.createInMemoryDb() - sessionRecord = SessionRecord( + sessionRecord = MockSession( id: TestConstants.sessionId, processId: ProcessIdentifier.current, state: .foreground, diff --git a/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift b/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift index f594ccc7..f3ec6386 100644 --- a/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift @@ -14,12 +14,12 @@ import OpenTelemetryApi final class SpansPayloadBuilderTests: XCTestCase { var storage: EmbraceStorage! - var sessionRecord: SessionRecord! + var sessionRecord: MockSession! override func setUpWithError() throws { storage = try EmbraceStorage.createInMemoryDb() - sessionRecord = SessionRecord( + sessionRecord = MockSession( id: TestConstants.sessionId, processId: .random, state: .foreground, @@ -75,7 +75,7 @@ final class SpansPayloadBuilderTests: XCTestCase { func test_noSessionSpan() throws { // given no session span and a session record with nil end time - let record = SessionRecord( + let record = MockSession( id: TestConstants.sessionId, processId: .random, state: .foreground, diff --git a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift index 381326c4..449f09bb 100644 --- a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift +++ b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift @@ -259,7 +259,7 @@ final class SessionControllerTests: XCTestCase { } func test_update_assignsAppTerminated_toFalse_whenPresent() throws { - let session = controller.startSession(state: .foreground) + var session = controller.startSession(state: .foreground) session!.appTerminated = true controller.update(appTerminated: false) @@ -267,7 +267,7 @@ final class SessionControllerTests: XCTestCase { } func test_update_assignsAppTerminated_toTrue_whenPresent() throws { - let session = controller.startSession(state: .foreground) + var session = controller.startSession(state: .foreground) session!.appTerminated = false controller.update(appTerminated: true) @@ -275,7 +275,7 @@ final class SessionControllerTests: XCTestCase { } func test_update_changesTo_appTerminated_saveInStorage() throws { - let session = controller.startSession(state: .foreground) + var session = controller.startSession(state: .foreground) session!.appTerminated = false controller.update(appTerminated: true) @@ -288,7 +288,7 @@ final class SessionControllerTests: XCTestCase { } func test_update_changesTo_sessionState_saveInStorage() throws { - let session = controller.startSession(state: .foreground) + var session = controller.startSession(state: .foreground) session!.appTerminated = false controller.update(state: .background) diff --git a/Tests/EmbraceCoreTests/Session/SessionSpanUtilsTests.swift b/Tests/EmbraceCoreTests/Session/SessionSpanUtilsTests.swift index cddde796..8dc144b0 100644 --- a/Tests/EmbraceCoreTests/Session/SessionSpanUtilsTests.swift +++ b/Tests/EmbraceCoreTests/Session/SessionSpanUtilsTests.swift @@ -3,7 +3,6 @@ // import XCTest - @testable import EmbraceCore import EmbraceCommonInternal import EmbraceStorageInternal @@ -126,7 +125,7 @@ final class SessionSpanUtilsTests: XCTestCase { let endTime = Date(timeIntervalSince1970: 60) let heartbeat = Date(timeIntervalSince1970: 58) - let session = SessionRecord( + let session = MockSession( id: TestConstants.sessionId, processId: TestConstants.processId, state: .foreground, @@ -203,7 +202,7 @@ final class SessionSpanUtilsTests: XCTestCase { func test_status() { // test ok status - var session = SessionRecord( + var session = MockSession( id: TestConstants.sessionId, processId: TestConstants.processId, state: .foreground, @@ -222,7 +221,7 @@ final class SessionSpanUtilsTests: XCTestCase { XCTAssertEqual(payload.status, "ok") // test error status - session = SessionRecord( + session = MockSession( id: TestConstants.sessionId, processId: TestConstants.processId, state: .foreground, @@ -272,7 +271,7 @@ final class SessionSpanUtilsTests: XCTestCase { func test_payloadFromSession_attributesShouldntIncludeUserProperties() { let session = givenSessionRecord() - var properties: [MetadataRecord] = [] + var properties: [EmbraceMetadata] = [] properties.append( givenCustomProperty( withKey: "emb.user.username", @@ -305,11 +304,11 @@ final class SessionSpanUtilsTests: XCTestCase { } private extension SessionSpanUtilsTests { - func givenSessionRecord() -> SessionRecord { + func givenSessionRecord() -> MockSession { let endTime = Date(timeIntervalSince1970: 60) let heartbeat = Date(timeIntervalSince1970: 58) - return SessionRecord( + return MockSession( id: TestConstants.sessionId, processId: TestConstants.processId, state: .foreground, @@ -324,8 +323,8 @@ private extension SessionSpanUtilsTests { appTerminated: .random()) } - func givenCustomProperty(withKey key: String, value: String, lifespan: MetadataRecordLifespan) -> MetadataRecord { - MetadataRecord( + func givenCustomProperty(withKey key: String, value: String, lifespan: MetadataRecordLifespan) -> MockMetadata { + MockMetadata( key: key, value: value, type: .customProperty, diff --git a/Tests/EmbraceCoreTests/TestSupport/Records/LogRecord+Init.swift b/Tests/EmbraceCoreTests/TestSupport/Records/LogRecord+Init.swift deleted file mode 100644 index ea213636..00000000 --- a/Tests/EmbraceCoreTests/TestSupport/Records/LogRecord+Init.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceCommonInternal -import EmbraceStorageInternal -import OpenTelemetryApi - -extension LogRecord { - convenience init( - id: LogIdentifier, - processId: ProcessIdentifier, - severity: LogSeverity, - body: String, - timestamp: Date = Date(), - attributes: [String: AttributeValue] - ) { - self.init() - - self.idRaw = id.toString - self.processIdRaw = processId.hex - self.severityRaw = severity.rawValue - self.body = body - self.timestamp = timestamp - - for (key, value) in attributes { - let attribute = LogAttributeRecord(key: key, value: value, log: self) - self.attributes.append(attribute) - } - } -} - -extension LogAttributeRecord { - convenience init( - key: String, - value: AttributeValue, - log: LogRecord - ) { - self.init() - - self.key = key - self.valueRaw = value.description - self.log = log - } -} diff --git a/Tests/EmbraceCoreTests/TestSupport/Records/MetadataRecord+Init.swift b/Tests/EmbraceCoreTests/TestSupport/Records/MetadataRecord+Init.swift deleted file mode 100644 index 8a2f4a61..00000000 --- a/Tests/EmbraceCoreTests/TestSupport/Records/MetadataRecord+Init.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceCommonInternal -import EmbraceStorageInternal - -extension MetadataRecord { - convenience init( - key: String, - value: String, - type: MetadataRecordType, - lifespan: MetadataRecordLifespan, - lifespanId: String, - collectedAt: Date = Date() - ) { - self.init() - - self.key = key - self.value = value - self.typeRaw = type.rawValue - self.lifespanRaw = lifespan.rawValue - self.lifespanId = lifespanId - self.collectedAt = collectedAt - } -} - - diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift index 1e76dc7a..e5bb76ee 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift @@ -7,23 +7,23 @@ import XCTest import EmbraceCommonInternal class MockMetadataFetcher: EmbraceStorageMetadataFetcher { - var metadata: [MetadataRecord] + var metadata: [EmbraceMetadata] - init(metadata: [MetadataRecord] = []) { + init(metadata: [EmbraceMetadata] = []) { self.metadata = metadata } - func fetchAllResources() -> [MetadataRecord] { + func fetchAllResources() -> [EmbraceMetadata] { return metadata } - func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { return metadata.filter { record in (record.type == .resource || record.type == .requiredResource) } } - func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { + func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { return metadata.filter { record in (record.type == .resource || record.type == .requiredResource) && record.lifespan == .process && @@ -31,7 +31,7 @@ class MockMetadataFetcher: EmbraceStorageMetadataFetcher { } } - func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { return metadata.filter { record in record.type == .customProperty && record.lifespan == .session && @@ -39,7 +39,7 @@ class MockMetadataFetcher: EmbraceStorageMetadataFetcher { } } - func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { return metadata.filter { record in record.type == .personaTag && record.lifespan == .session && @@ -47,7 +47,7 @@ class MockMetadataFetcher: EmbraceStorageMetadataFetcher { } } - func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { + func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { return metadata.filter { record in record.type == .personaTag && record.lifespan == .process && diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift index d0bf6cf1..09d0025d 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift @@ -16,20 +16,20 @@ class MockSessionController: SessionControllable { var didCallEndSession: Bool = false var didCallUpdateSession: Bool = false - private var updateSessionCallback: ((SessionRecord?, SessionState?, Bool?) -> Void)? + private var updateSessionCallback: ((EmbraceSession?, SessionState?, Bool?) -> Void)? weak var storage: EmbraceStorage? - var currentSession: SessionRecord? + var currentSession: EmbraceSession? func clear() { } @discardableResult - func startSession(state: SessionState) -> SessionRecord? { + func startSession(state: SessionState) -> EmbraceSession? { return startSession(state: state, startTime: Date()) } @discardableResult - func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord? { + func startSession(state: SessionState, startTime: Date = Date()) -> EmbraceSession? { if currentSession != nil { endSession() } @@ -69,7 +69,7 @@ class MockSessionController: SessionControllable { updateSessionCallback?(currentSession, nil, appTerminated) } - func onUpdateSession(_ callback: @escaping ((SessionRecord?, SessionState?, Bool?) -> Void)) { + func onUpdateSession(_ callback: @escaping ((EmbraceSession?, SessionState?, Bool?) -> Void)) { updateSessionCallback = callback } diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogBatcherDelegate.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogBatcherDelegate.swift index c392b54c..94efea28 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogBatcherDelegate.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogBatcherDelegate.swift @@ -2,13 +2,12 @@ // Copyright © 2023 Embrace Mobile, Inc. All rights reserved. // -import EmbraceStorageInternal - +import EmbraceCommonInternal @testable import EmbraceCore class SpyLogBatcherDelegate: LogBatcherDelegate { var didCallBatchFinished: Bool = false - func batchFinished(withLogs logs: [LogRecord]) { + func batchFinished(withLogs logs: [EmbraceLog]) { didCallBatchFinished = true } } diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift index 73d59adb..7ee0b378 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift @@ -6,18 +6,19 @@ import Foundation import EmbraceStorageInternal import EmbraceCommonInternal import OpenTelemetryApi +import TestSupport class SpyLogRepository: LogRepository { var didCallFetchAll = false - var stubbedFetchAllResult: [LogRecord] = [] - func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [LogRecord] { + var stubbedFetchAllResult: [EmbraceLog] = [] + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] { didCallFetchAll = true return stubbedFetchAllResult } var didCallRemoveLogs = false - func remove(logs: [LogRecord]) { + func remove(logs: [EmbraceLog]) { didCallRemoveLogs = true } @@ -34,10 +35,10 @@ class SpyLogRepository: LogRepository { body: String, timestamp: Date, attributes: [String : AttributeValue] - ) -> LogRecord { + ) -> EmbraceLog { didCallCreate = true - return LogRecord( + return MockLog( id: id, processId: processId, severity: severity, diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift index 0475ee07..ef78f75a 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift @@ -6,6 +6,7 @@ import Foundation import EmbraceStorageInternal import EmbraceCommonInternal import OpenTelemetryApi +import TestSupport class RandomError: Error, CustomNSError { static var errorDomain: String = "Embrace" @@ -16,16 +17,16 @@ class RandomError: Error, CustomNSError { class SpyStorage: Storage { var didCallFetchAllResources = false - var stubbedFetchAllResources: [MetadataRecord] = [] - func fetchAllResources() -> [MetadataRecord] { + var stubbedFetchAllResources: [EmbraceMetadata] = [] + func fetchAllResources() -> [EmbraceMetadata] { didCallFetchAllResources = true return stubbedFetchAllResources } var didCallFetchResourcesForSessionId = false var fetchResourcesForSessionIdReceivedParameter: SessionIdentifier! - var stubbedFetchResourcesForSessionId: [MetadataRecord] = [] - func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + var stubbedFetchResourcesForSessionId: [EmbraceMetadata] = [] + func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { didCallFetchResourcesForSessionId = true fetchResourcesForSessionIdReceivedParameter = sessionId return stubbedFetchResourcesForSessionId @@ -33,8 +34,8 @@ class SpyStorage: Storage { var didCallFetchResourcesForProcessId = false var fetchResourcesForProcessIdReceivedParameter: ProcessIdentifier! - var stubbedFetchResourcesForProcessId: [MetadataRecord] = [] - func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { + var stubbedFetchResourcesForProcessId: [EmbraceMetadata] = [] + func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { didCallFetchResourcesForProcessId = true fetchResourcesForProcessIdReceivedParameter = processId return stubbedFetchResourcesForProcessId @@ -42,8 +43,8 @@ class SpyStorage: Storage { var didCallFetchCustomPropertiesForSessionId = false var fetchCustomPropertiesForSessionIdReceivedParameter: SessionIdentifier! - var stubbedFetchCustomPropertiesForSessionId: [MetadataRecord] = [] - func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + var stubbedFetchCustomPropertiesForSessionId: [EmbraceMetadata] = [] + func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { didCallFetchCustomPropertiesForSessionId = true fetchCustomPropertiesForSessionIdReceivedParameter = sessionId return stubbedFetchCustomPropertiesForSessionId @@ -51,8 +52,8 @@ class SpyStorage: Storage { var didCallFetchPersonaTagsForSessionId = false var fetchPersonaTagsForSessionIdReceivedParameter: SessionIdentifier! - var stubbedFetchPersonaTagsForSessionId: [MetadataRecord] = [] - func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [MetadataRecord] { + var stubbedFetchPersonaTagsForSessionId: [EmbraceMetadata] = [] + func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { didCallFetchPersonaTagsForSessionId = true fetchPersonaTagsForSessionIdReceivedParameter = sessionId return stubbedFetchPersonaTagsForSessionId @@ -60,8 +61,8 @@ class SpyStorage: Storage { var didCallFetchPersonaTagsForProcessId = false var fetchPersonaTagsForProcessIdReceivedParameter: ProcessIdentifier! - var stubbedFetchPersonaTagsForProcessId: [MetadataRecord] = [] - func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [MetadataRecord] { + var stubbedFetchPersonaTagsForProcessId: [EmbraceMetadata] = [] + func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { didCallFetchPersonaTagsForProcessId = true fetchPersonaTagsForProcessIdReceivedParameter = processId return stubbedFetchPersonaTagsForProcessId @@ -75,10 +76,10 @@ class SpyStorage: Storage { body: String, timestamp: Date, attributes: [String : AttributeValue] - ) -> LogRecord { + ) -> EmbraceLog { didCallCreate = true - return LogRecord( + return MockLog( id: id, processId: processId, severity: severity, @@ -89,17 +90,17 @@ class SpyStorage: Storage { } var didCallFetchAllExcludingProcessIdentifier = false - var stubbedFetchAllExcludingProcessIdentifier: [LogRecord] = [] + var stubbedFetchAllExcludingProcessIdentifier: [EmbraceLog] = [] var fetchAllExcludingProcessIdentifierReceivedParameter: ProcessIdentifier! - func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [LogRecord] { + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] { didCallFetchAllExcludingProcessIdentifier = true fetchAllExcludingProcessIdentifierReceivedParameter = processIdentifier return stubbedFetchAllExcludingProcessIdentifier } var didCallRemoveLogs = false - var removeLogsReceivedParameter: [LogRecord] = [] - func remove(logs: [LogRecord]) { + var removeLogsReceivedParameter: [EmbraceLog] = [] + func remove(logs: [EmbraceLog]) { didCallRemoveLogs = true removeLogsReceivedParameter = logs } diff --git a/Tests/EmbraceCoreTests/TestSupport/Utilities/MetadataRecord+Factory.swift b/Tests/EmbraceCoreTests/TestSupport/Utilities/MetadataRecord+Factory.swift deleted file mode 100644 index d2b461c4..00000000 --- a/Tests/EmbraceCoreTests/TestSupport/Utilities/MetadataRecord+Factory.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceStorageInternal -import EmbraceCommonInternal -import OpenTelemetryApi - -extension MetadataRecord { - static func createSessionPropertyRecord( - key: String, - value: AttributeValue, - sessionId: SessionIdentifier = .random - ) -> MetadataRecord { - MetadataRecord( - key: key, - value: value.description, - type: .customProperty, - lifespan: .session, - lifespanId: sessionId.toString - ) - } - - static func userMetadata(key: String, value: String) -> MetadataRecord { - MetadataRecord( - key: key, - value: value, - type: .customProperty, - lifespan: .session, - lifespanId: .random() - ) - } - - static func createResourceRecord(key: String, value: String) -> MetadataRecord { - MetadataRecord( - key: key, - value: value, - type: .resource, - lifespan: .session, - lifespanId: .random() - ) - } - - static func createPersonaTagRecord(value: String) -> MetadataRecord { - MetadataRecord( - key: value, - value: value, - type: .personaTag, - lifespan: .session, - lifespanId: .random() - ) - } -} diff --git a/Tests/EmbraceCoreTests/TestSupport/Utilities/SessionRecord+Factory.swift b/Tests/EmbraceCoreTests/TestSupport/Utilities/SessionRecord+Factory.swift deleted file mode 100644 index 0637f990..00000000 --- a/Tests/EmbraceCoreTests/TestSupport/Utilities/SessionRecord+Factory.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceStorageInternal -import EmbraceCommonInternal - -extension SessionRecord { - static func with(id: SessionIdentifier, state: SessionState) -> SessionRecord { - SessionRecord( - id: id, - processId: .random, - state: state, - traceId: "", - spanId: "", - startTime: Date() - ) - } -} diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift index 65540aea..c2c84f0d 100644 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift +++ b/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift @@ -90,7 +90,7 @@ class EmbraceStorageLoggingTests: XCTestCase { private extension EmbraceStorageLoggingTests { @discardableResult - func createInfoLog(withId id: UUID = UUID(), pid: ProcessIdentifier = .random) -> LogRecord { + func createInfoLog(withId id: UUID = UUID(), pid: ProcessIdentifier = .random) -> EmbraceLog { return sut.createLog( id: LogIdentifier.init(value: id), processId: pid, diff --git a/Tests/TestSupport/Mocks/DummyLogControllable.swift b/Tests/TestSupport/Mocks/DummyLogControllable.swift index c38e58fc..70332c52 100644 --- a/Tests/TestSupport/Mocks/DummyLogControllable.swift +++ b/Tests/TestSupport/Mocks/DummyLogControllable.swift @@ -26,5 +26,5 @@ public class DummyLogControllable: LogControllable { stackTraceBehavior: StackTraceBehavior ) { } - public func batchFinished(withLogs logs: [LogRecord]) {} + public func batchFinished(withLogs logs: [EmbraceLog]) {} } diff --git a/Tests/TestSupport/Mocks/Model/MockLog.swift b/Tests/TestSupport/Mocks/Model/MockLog.swift new file mode 100644 index 00000000..d1a7155f --- /dev/null +++ b/Tests/TestSupport/Mocks/Model/MockLog.swift @@ -0,0 +1,68 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import OpenTelemetryApi + +public class MockLog: EmbraceLog { + public var idRaw: String + public var processIdRaw: String + public var severityRaw: Int + public var body: String + public var timestamp: Date + public var attributes: [MockLogAttribute] + + public init( + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date = Date(), + attributes: [String: AttributeValue] = [:] + ) { + self.idRaw = id.toString + self.processIdRaw = processId.hex + self.severityRaw = severity.rawValue + self.body = body + self.timestamp = timestamp + + var finalAttributes: [MockLogAttribute] = [] + for (key, value) in attributes { + let attribute = MockLogAttribute(key: key, value: value) + finalAttributes.append(attribute) + } + self.attributes = finalAttributes + } + + public func allAttributes() -> [any EmbraceLogAttribute] { + return attributes + } + + public func attribute(forKey key: String) -> (any EmbraceLogAttribute)? { + return attributes.first(where: { $0.key == key }) + } + + public func setAttributeValue(value: AttributeValue, forKey key: String) { + if var attribute = attribute(forKey: key) { + attribute.value = value + return + } + + let attribute = MockLogAttribute(key: key, value: value) + attributes.append(attribute) + } +} + +public class MockLogAttribute: EmbraceLogAttribute { + public var key: String + public var valueRaw: String = "" + public var typeRaw: Int = 0 + + public init(key: String, value: AttributeValue) { + self.key = key + self.valueRaw = value.description + self.typeRaw = typeForValue(value).rawValue + } +} diff --git a/Tests/TestSupport/Mocks/Model/MockMetadata.swift b/Tests/TestSupport/Mocks/Model/MockMetadata.swift new file mode 100644 index 00000000..a5f94a1b --- /dev/null +++ b/Tests/TestSupport/Mocks/Model/MockMetadata.swift @@ -0,0 +1,78 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import OpenTelemetryApi + +public class MockMetadata: EmbraceMetadata { + public var key: String + public var value: String + public var typeRaw: String + public var lifespanRaw: String + public var lifespanId: String + public var collectedAt: Date + + public init( + key: String, + value: String, + type: MetadataRecordType, + lifespan: MetadataRecordLifespan, + lifespanId: String = "", + collectedAt: Date = Date() + ) { + self.key = key + self.value = value + self.typeRaw = type.rawValue + self.lifespanRaw = lifespan.rawValue + self.lifespanId = lifespanId + self.collectedAt = collectedAt + } +} + +public extension MockMetadata { + static func createSessionPropertyRecord( + key: String, + value: AttributeValue, + sessionId: SessionIdentifier = .random + ) -> EmbraceMetadata { + MockMetadata( + key: key, + value: value.description, + type: .customProperty, + lifespan: .session, + lifespanId: sessionId.toString + ) + } + + static func createUserMetadata(key: String, value: String) -> EmbraceMetadata { + MockMetadata( + key: key, + value: value, + type: .customProperty, + lifespan: .session, + lifespanId: SessionIdentifier.random.toString + ) + } + + static func createResourceRecord(key: String, value: String) -> EmbraceMetadata { + MockMetadata( + key: key, + value: value, + type: .resource, + lifespan: .session, + lifespanId: SessionIdentifier.random.toString + ) + } + + static func createPersonaTagRecord(value: String) -> EmbraceMetadata { + MockMetadata( + key: value, + value: value, + type: .personaTag, + lifespan: .session, + lifespanId: SessionIdentifier.random.toString + ) + } +} diff --git a/Tests/EmbraceCoreTests/TestSupport/Records/SessionRecord+Init.swift b/Tests/TestSupport/Mocks/Model/MockSession.swift similarity index 52% rename from Tests/EmbraceCoreTests/TestSupport/Records/SessionRecord+Init.swift rename to Tests/TestSupport/Mocks/Model/MockSession.swift index f31fa720..00ce6c10 100644 --- a/Tests/EmbraceCoreTests/TestSupport/Records/SessionRecord+Init.swift +++ b/Tests/TestSupport/Mocks/Model/MockSession.swift @@ -4,10 +4,22 @@ import Foundation import EmbraceCommonInternal -import EmbraceStorageInternal -extension SessionRecord { - convenience init( +public class MockSession: EmbraceSession { + public var idRaw: String + public var processIdRaw: String + public var state: String + public var traceId: String + public var spanId: String + public var startTime: Date + public var endTime: Date? + public var lastHeartbeatTime: Date + public var crashReportId: String? + public var coldStart: Bool + public var cleanExit: Bool + public var appTerminated: Bool + + public init( id: SessionIdentifier, processId: ProcessIdentifier, state: SessionState, @@ -21,8 +33,6 @@ extension SessionRecord { cleanExit: Bool = false, appTerminated: Bool = false ) { - self.init() - self.idRaw = id.toString self.processIdRaw = processId.hex self.state = state.rawValue @@ -30,10 +40,23 @@ extension SessionRecord { self.spanId = spanId self.startTime = startTime self.endTime = endTime - self.lastHeartbeatTime = ((lastHeartbeatTime ?? endTime) ?? startTime) + self.lastHeartbeatTime = lastHeartbeatTime ?? (endTime ?? startTime) self.crashReportId = crashReportId self.coldStart = coldStart self.cleanExit = cleanExit self.appTerminated = appTerminated } } + +public extension MockSession { + static func with(id: SessionIdentifier, state: SessionState) -> MockSession { + MockSession( + id: id, + processId: .random, + state: state, + traceId: "", + spanId: "", + startTime: Date() + ) + } +} From 57e76daa480562dc4c57a267448498ff96486d7f Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Thu, 6 Feb 2025 14:08:58 -0300 Subject: [PATCH 13/17] WIP --- .../Logs/Exporter/DefaultLogBatcher.swift | 7 +- .../Session/SessionController.swift | 2 +- .../CoreDataWrapper.swift | 110 ++++++++++-------- .../Records/EmbraceStorage+Log.swift | 13 ++- .../Records/EmbraceStorage+Metadata.swift | 11 +- .../Records/EmbraceStorage+Session.swift | 13 ++- .../Records/EmbraceStorage+Span.swift | 23 ++-- .../Records/LogAttributeRecord.swift | 8 +- .../Records/LogRecord.swift | 20 ++-- .../Records/MetadataRecord.swift | 8 +- .../Records/SessionRecord.swift | 8 +- .../Records/SpanRecord.swift | 12 +- .../Cache/EmbraceUploadCache.swift | 19 +-- .../Cache/UploadDataRecord.swift | 8 +- .../AppInfoCaptureServiceTests.swift | 4 +- .../Internal/Logs/LogControllerTests.swift | 18 --- .../Public/EmbraceCoreTests.swift | 3 +- .../Metadata/MetadataHandler+UserTests.swift | 1 + .../Metadata/MetadataHandlerTests.swift | 12 +- .../Session/SessionControllerTests.swift | 1 + .../Session/UnsentDataHandlerTests.swift | 6 +- .../TestDoubles/MockSessionController.swift | 32 +++-- .../TestDoubles/SpyLogRepository.swift | 2 +- .../TestSupport/TestDoubles/SpyStorage.swift | 2 +- .../EmbraceStorageLoggingTests.swift | 2 +- .../EmbraceStorageTests.swift | 6 +- ...aceStorage+SpanForSessionRecordTests.swift | 6 +- .../SpanRecordTests.swift | 2 +- ...mbraceUploadCacheTests+ClearDataDate.swift | 36 +++--- .../EmbraceUploadCacheTests.swift | 6 +- .../Extensions/EmbraceStorage+Extension.swift | 2 +- 31 files changed, 225 insertions(+), 178 deletions(-) diff --git a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift index 19d42872..1f0865e7 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift @@ -41,15 +41,16 @@ class DefaultLogBatcher: LogBatcher { func addLogRecord(logRecord: ReadableLogRecord) { processorQueue.async { - let record = self.repository.createLog( + if let record = self.repository.createLog( id: LogIdentifier(), processId: ProcessIdentifier.current, severity: logRecord.severity?.toLogSeverity() ?? .info, body: logRecord.body?.description ?? "", timestamp: logRecord.timestamp, attributes: logRecord.attributes - ) - self.addLogToBatch(record) + ) { + self.addLogToBatch(record) + } } } } diff --git a/Sources/EmbraceCore/Session/SessionController.swift b/Sources/EmbraceCore/Session/SessionController.swift index 0df65ced..dd5e751b 100644 --- a/Sources/EmbraceCore/Session/SessionController.swift +++ b/Sources/EmbraceCore/Session/SessionController.swift @@ -137,7 +137,7 @@ class SessionController: SessionControllable { spanId: span.context.spanId.hexString, startTime: startTime ) - session.coldStart = isColdStart + session?.coldStart = isColdStart currentSession = session // save session record diff --git a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index db2012a5..d3ee7c8f 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -14,6 +14,7 @@ public class CoreDataWrapper { public private(set) var context: NSManagedObjectContext! let logger: InternalLogger + let lock = NSLock() public init(options: CoreDataWrapper.Options, logger: InternalLogger) throws { self.options = options @@ -55,82 +56,97 @@ public class CoreDataWrapper { /// - Note: Only used in tests!!! public func destroy() { #if canImport(XCTest) - context.reset() - - switch options.storageMechanism { - case .onDisk: - if let url = options.storageMechanism.fileURL { - do { - try container.persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType) - try FileManager.default.removeItem(at: url) - } catch { - logger.error("Error destroying CoreData stack!:\n\(error.localizedDescription)") + lock.withLock { + context.performAndWait { + + context.reset() + + switch options.storageMechanism { + case .onDisk: + if let url = options.storageMechanism.fileURL { + do { + try container.persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType) + try FileManager.default.removeItem(at: url) + } catch { + logger.error("Error destroying CoreData stack!:\n\(error.localizedDescription)") + } + } + + default: return } - } - default: return - } + if let store = container.persistentStoreCoordinator.persistentStores.first { + do { + try container.persistentStoreCoordinator.remove(store) + } catch { + logger.error("Error removing CoreData store!:\n\(error.localizedDescription)") + } + } - if let store = container.persistentStoreCoordinator.persistentStores.first { - do { - try container.persistentStoreCoordinator.remove(store) - } catch { - logger.error("Error removing CoreData store!:\n\(error.localizedDescription)") + container = nil + context = nil } } - - container = nil - context = nil #endif } - /// Asynchronously saves all changes on the current context to disk + /// Synchronously saves all changes on the current context to disk public func save() { - context.perform { [weak self] in - do { - try self?.context.save() - } catch { - let name = self?.context.name ?? "???" - self?.logger.warning("Error saving CoreData \"\(name)\": \(error.localizedDescription)") + lock.withLock { + context.performAndWait { [weak self] in + do { + try self?.context.save() + } catch { + let name = self?.context.name ?? "???" + self?.logger.warning("Error saving CoreData \"\(name)\": \(error.localizedDescription)") + } } } } /// Synchronously fetches the records that satisfy the given request public func fetch(withRequest request: NSFetchRequest) -> [T] where T: NSManagedObject { - var result: [T] = [] - context.performAndWait { - do { - result = try context.fetch(request) - } catch { } + return lock.withLock { + + var result: [T] = [] + context.performAndWait { + do { + result = try context.fetch(request) + } catch { } + } + return result } - return result } /// Synchronously fetches the count of records that satisfy the given request public func count(withRequest request: NSFetchRequest) -> Int where T: NSManagedObject { - var result: Int = 0 - context.performAndWait { - do { - result = try context.count(for: request) - } catch { } + return lock.withLock { + + var result: Int = 0 + context.performAndWait { + do { + result = try context.count(for: request) + } catch { } + } + return result } - return result } - /// Asynchronously deletes record from the database and saves + /// Synchronously deletes record from the database and saves public func deleteRecord(_ record: T) where T: NSManagedObject { deleteRecords([record]) } - /// Asynchronously deletes requested records from the database and saves + /// Synchronously deletes requested records from the database and saves public func deleteRecords(_ records: [T]) where T: NSManagedObject { - context.perform { [weak self] in - for record in records { - self?.context.delete(record) + lock.withLock { + context.performAndWait { [weak self] in + for record in records { + self?.context.delete(record) + } + + try? self?.context.save() } - - self?.save() } } } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift index b41be52c..9b94718e 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift @@ -14,7 +14,7 @@ public protocol LogRepository { body: String, timestamp: Date, attributes: [String: AttributeValue] - ) -> EmbraceLog + ) -> EmbraceLog? func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] func remove(logs: [EmbraceLog]) func removeAllLogs() @@ -30,8 +30,8 @@ extension EmbraceStorage { body: String, timestamp: Date = Date(), attributes: [String: OpenTelemetryApi.AttributeValue] - ) -> EmbraceLog { - return LogRecord.create( + ) -> EmbraceLog? { + if let log = LogRecord.create( context: coreData.context, id: id, processId: processId, @@ -39,7 +39,12 @@ extension EmbraceStorage { body: body, timestamp: timestamp, attributes: attributes - ) + ) { + coreData.save() + return log + } + + return nil } public func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] { diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift index 667b28cb..8b3af64e 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift @@ -43,18 +43,19 @@ extension EmbraceStorage { return nil } - let metadata = MetadataRecord.create( + if let metadata = MetadataRecord.create( context: coreData.context, key: key, value: value, type: type, lifespan: lifespan, lifespanId: lifespanId - ) - - coreData.save() + ) { + coreData.save() + return metadata + } - return metadata + return nil } /// Returns the `MetadataRecord` for the given values. diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift index cd094dc0..fd426ce3 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift @@ -32,7 +32,7 @@ extension EmbraceStorage { coldStart: Bool = false, cleanExit: Bool = false, appTerminated: Bool = false - ) -> SessionRecord { + ) -> SessionRecord? { // update existing? if let session = fetchSession(id: id) { @@ -56,7 +56,7 @@ extension EmbraceStorage { } // create new - let session = SessionRecord.create( + if let session = SessionRecord.create( context: coreData.context, id: id, processId: processId, @@ -69,11 +69,12 @@ extension EmbraceStorage { coldStart: coldStart, cleanExit: cleanExit, appTerminated: appTerminated - ) - - coreData.save() + ) { + coreData.save() + return session + } - return session + return nil } /// Fetches the stored `SessionRecord` synchronously with the given identifier, if any. diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index 9927d6ce..4f836773 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -30,8 +30,8 @@ extension EmbraceStorage { data: Data, startTime: Date, endTime: Date? = nil, - processIdentifier: ProcessIdentifier = .current - ) -> SpanRecord { + processId: ProcessIdentifier = .current + ) -> SpanRecord? { // update existing? if let span = fetchSpan(id: id, traceId: traceId) { @@ -40,7 +40,7 @@ extension EmbraceStorage { span.data = data span.startTime = startTime span.endTime = endTime - span.processIdRaw = processIdentifier.hex + span.processIdRaw = processId.hex coreData.save() return span @@ -50,7 +50,7 @@ extension EmbraceStorage { removeOldSpanIfNeeded(forType: type) // add new - let span = SpanRecord.create( + if let span = SpanRecord.create( context: coreData.context, id: id, name: name, @@ -59,12 +59,13 @@ extension EmbraceStorage { data: data, startTime: startTime, endTime: endTime, - processIdentifier: processIdentifier - ) - - coreData.save() + processId: processId + ) { + coreData.save() + return span + } - return span + return nil } /// Fetches the stored `SpanRecord` synchronously with the given identifiers, if any. @@ -147,7 +148,7 @@ extension EmbraceStorage { // - ends before session ends or // - hasn't ended yet let predicate1 = NSPredicate( - format: "(startTime >= %@ AND (endTime = nil OR endTime <= %@)", + format: "startTime >= %@ AND (endTime = nil OR endTime <= %@)", startTime, endTime ) @@ -156,7 +157,7 @@ extension EmbraceStorage { // - ends within session or // - hasn't ended yet let predicate2 = NSPredicate( - format: "(startTime < %@ AND (endTime = nil OR (endTime >= %@ AND endTime <= %@))", + format: "startTime < %@ AND (endTime = nil OR (endTime >= %@ AND endTime <= %@))", startTime, startTime, endTime diff --git a/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift index 0a23394a..0a87b248 100644 --- a/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift @@ -18,8 +18,12 @@ public class LogAttributeRecord: NSManagedObject, EmbraceLogAttribute { key: String, value: AttributeValue, log: LogRecord - ) -> LogAttributeRecord { - var record = LogAttributeRecord(context: context) + ) -> LogAttributeRecord? { + guard let description = NSEntityDescription.entity(forEntityName: Self.entityName, in: context) else { + return nil + } + + var record = LogAttributeRecord(entity: description, insertInto: context) record.key = key record.value = value record.log = log diff --git a/Sources/EmbraceStorageInternal/Records/LogRecord.swift b/Sources/EmbraceStorageInternal/Records/LogRecord.swift index 0f7d8a0e..506be9f8 100644 --- a/Sources/EmbraceStorageInternal/Records/LogRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/LogRecord.swift @@ -23,8 +23,12 @@ public class LogRecord: NSManagedObject, EmbraceLog { body: String, timestamp: Date = Date(), attributes: [String: AttributeValue] - ) -> LogRecord { - let record = LogRecord(context: context) + ) -> LogRecord? { + guard let description = NSEntityDescription.entity(forEntityName: Self.entityName, in: context) else { + return nil + } + + let record = LogRecord(entity: description, insertInto: context) record.idRaw = id.toString record.processIdRaw = processId.hex record.severityRaw = severity.rawValue @@ -32,13 +36,14 @@ public class LogRecord: NSManagedObject, EmbraceLog { record.timestamp = timestamp for (key, value) in attributes { - let attribute = LogAttributeRecord.create( + if let attribute = LogAttributeRecord.create( context: context, key: key, value: value, log: record - ) - record.attributes.append(attribute) + ) { + record.attributes.append(attribute) + } } return record @@ -66,8 +71,9 @@ public class LogRecord: NSManagedObject, EmbraceLog { return } - let attribute = LogAttributeRecord.create(context: context, key: key, value: value, log: self) - attributes.append(attribute) + if let attribute = LogAttributeRecord.create(context: context, key: key, value: value, log: self) { + attributes.append(attribute) + } } } diff --git a/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift b/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift index c0b49ad8..eafacd9e 100644 --- a/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift @@ -23,8 +23,12 @@ public class MetadataRecord: NSManagedObject, EmbraceMetadata { lifespan: MetadataRecordLifespan, lifespanId: String, collectedAt: Date = Date() - ) -> MetadataRecord { - let record = MetadataRecord(context: context) + ) -> MetadataRecord? { + guard let description = NSEntityDescription.entity(forEntityName: Self.entityName, in: context) else { + return nil + } + + let record = MetadataRecord(entity: description, insertInto: context) record.key = key record.value = value record.typeRaw = type.rawValue diff --git a/Sources/EmbraceStorageInternal/Records/SessionRecord.swift b/Sources/EmbraceStorageInternal/Records/SessionRecord.swift index 30700839..5f02f445 100644 --- a/Sources/EmbraceStorageInternal/Records/SessionRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/SessionRecord.swift @@ -41,8 +41,12 @@ public class SessionRecord: NSManagedObject, EmbraceSession { coldStart: Bool = false, cleanExit: Bool = false, appTerminated: Bool = false - ) -> SessionRecord { - let record = SessionRecord(context: context) + ) -> SessionRecord? { + guard let description = NSEntityDescription.entity(forEntityName: Self.entityName, in: context) else { + return nil + } + + let record = SessionRecord(entity: description, insertInto: context) record.idRaw = id.toString record.processIdRaw = processId.hex record.state = state.rawValue diff --git a/Sources/EmbraceStorageInternal/Records/SpanRecord.swift b/Sources/EmbraceStorageInternal/Records/SpanRecord.swift index 0d0d77f1..a0bcbab3 100644 --- a/Sources/EmbraceStorageInternal/Records/SpanRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/SpanRecord.swift @@ -26,9 +26,13 @@ public class SpanRecord: NSManagedObject, EmbraceSpan { data: Data, startTime: Date, endTime: Date? = nil, - processIdentifier: ProcessIdentifier - ) -> SpanRecord { - let record = SpanRecord(context: context) + processId: ProcessIdentifier + ) -> SpanRecord? { + guard let description = NSEntityDescription.entity(forEntityName: Self.entityName, in: context) else { + return nil + } + + let record = SpanRecord(entity: description, insertInto: context) record.id = id record.name = name record.traceId = traceId @@ -36,7 +40,7 @@ public class SpanRecord: NSManagedObject, EmbraceSpan { record.data = data record.startTime = startTime record.endTime = endTime - record.processIdRaw = processIdentifier.hex + record.processIdRaw = processId.hex return record } diff --git a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift index 19c8fb21..e63200bb 100644 --- a/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift +++ b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift @@ -90,9 +90,9 @@ class EmbraceUploadCache { // update if it already exists if let record = fetchUploadData(id: id, type: type) { - coreData.context.perform { [weak self] in + coreData.context.performAndWait { [weak self] in record.data = data - self?.coreData.save() + try? self?.coreData.context.save() } return true @@ -117,7 +117,12 @@ class EmbraceUploadCache { do { try coreData.context.save() } catch { - coreData.context.delete(record) + logger.error("Error saving cache data!:\n\(error.localizedDescription)") + + if let record = record { + coreData.context.delete(record) + } + result = false } } @@ -132,7 +137,7 @@ class EmbraceUploadCache { return } - coreData.context.perform { [weak self] in + coreData.context.performAndWait { [weak self] in guard let strongSelf = self else { return } @@ -150,7 +155,7 @@ class EmbraceUploadCache { strongSelf.coreData.context.delete(uploadData) } - strongSelf.coreData.save() + try strongSelf.coreData.context.save() } } catch { } } @@ -183,9 +188,9 @@ class EmbraceUploadCache { return } - coreData.context.perform { [weak self] in + coreData.context.performAndWait { [weak self] in uploadData.attemptCount = attemptCount - self?.coreData.save() + try? self?.coreData.context.save() } } diff --git a/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift b/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift index c3e2ab47..dba78160 100644 --- a/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift +++ b/Sources/EmbraceUploadInternal/Cache/UploadDataRecord.swift @@ -21,8 +21,12 @@ public class UploadDataRecord: NSManagedObject { attemptCount: Int, date: Date - ) -> UploadDataRecord { - let record = UploadDataRecord(context: context) + ) -> UploadDataRecord? { + guard let description = NSEntityDescription.entity(forEntityName: Self.entityName, in: context) else { + return nil + } + + let record = UploadDataRecord(entity: description, insertInto: context) record.id = id record.type = type record.data = data diff --git a/Tests/EmbraceCoreTests/Capture/OneTimeServices/AppInfoCaptureServiceTests.swift b/Tests/EmbraceCoreTests/Capture/OneTimeServices/AppInfoCaptureServiceTests.swift index 8d5899e1..323580da 100644 --- a/Tests/EmbraceCoreTests/Capture/OneTimeServices/AppInfoCaptureServiceTests.swift +++ b/Tests/EmbraceCoreTests/Capture/OneTimeServices/AppInfoCaptureServiceTests.swift @@ -13,7 +13,7 @@ final class AppInfoCaptureServiceTests: XCTestCase { func test_started() throws { // given an app info capture service let service = AppInfoCaptureService() - let handler = try EmbraceStorage.createInDiskDb(fileName: testName) + let handler = try EmbraceStorage.createInMemoryDb() service.handler = handler // when the service is installed and started @@ -97,7 +97,7 @@ final class AppInfoCaptureServiceTests: XCTestCase { func test_notStarted() throws { // given an app info capture service let service = AppInfoCaptureService() - let handler = try EmbraceStorage.createInDiskDb(fileName: testName) + let handler = try EmbraceStorage.createInMemoryDb() service.handler = handler // when the service is installed but not started diff --git a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift index ae0bd145..b039dd8c 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift @@ -48,13 +48,6 @@ class LogControllerTests: XCTestCase { thenDoesntTryToUploadAnything() } - func testHavingThrowingFetchAll_onSetup_shouldRemoveAllLogs() throws { - givenStorageThatThrowsException() - givenLogController() - whenInvokingSetup() - try thenStorageShouldHaveRemoveAllLogs() - } - func testHavingLogs_onSetup_fetchesResourcesFromStorage() throws { let sessionId = SessionIdentifier.random let log = randomLogRecord(sessionId: sessionId) @@ -173,13 +166,6 @@ class LogControllerTests: XCTestCase { thenDoesntTryToUploadAnything() } - func testHavingThrowingStorage_onBatchFinished_wontTryToUploadAnything() { - givenStorageThatThrowsException() - givenLogController() - whenInvokingBatchFinished(withLogs: [randomLogRecord()]) - thenDoesntTryToUploadAnything() - } - func test_onBatchFinishedReceivingLogsAmountLargerThanBatch_logUploaderShouldSendASingleBatch() { givenLogController() let logs = (0...(LogController.maxLogsPerBatch + 5)).map { _ in randomLogRecord() } @@ -311,10 +297,6 @@ private extension LogControllerTests { storage?.stubbedFetchAllExcludingProcessIdentifier = logs } - func givenStorageThatThrowsException() { - storage = .init(SpyStorage()) - } - func whenInvokingSetup() { sut.uploadAllPersistedLogs() } diff --git a/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift b/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift index 0b865b5d..fb8f1507 100644 --- a/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift +++ b/Tests/EmbraceCoreTests/Public/EmbraceCoreTests.swift @@ -8,7 +8,7 @@ import EmbraceCommonInternal import EmbraceStorageInternal import EmbraceOTelInternal import OpenTelemetrySdk - +/* final class EmbraceCoreTests: XCTestCase { // this is used in the helper function @@ -314,3 +314,4 @@ final class EmbraceCoreTests: XCTestCase { return String((0.. EmbraceLog { + ) -> EmbraceLog? { didCallCreate = true return MockLog( diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift index ef78f75a..c37ca0a0 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift @@ -76,7 +76,7 @@ class SpyStorage: Storage { body: String, timestamp: Date, attributes: [String : AttributeValue] - ) -> EmbraceLog { + ) -> EmbraceLog? { didCallCreate = true return MockLog( diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift index c2c84f0d..edac14aa 100644 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift +++ b/Tests/EmbraceStorageInternalTests/EmbraceStorageLoggingTests.swift @@ -97,6 +97,6 @@ private extension EmbraceStorageLoggingTests { severity: .info, body: "a log message", attributes: .empty() - ) + )! } } diff --git a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift index 8dfa959b..489126f3 100644 --- a/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift +++ b/Tests/EmbraceStorageInternalTests/EmbraceStorageTests.swift @@ -26,7 +26,7 @@ class EmbraceStorageTests: XCTestCase { type: .performance, data: Data(), startTime: Date() - ) + )! // then record should exist in storage var spans: [SpanRecord] = storage.fetchAll() @@ -50,7 +50,7 @@ class EmbraceStorageTests: XCTestCase { type: .performance, data: Data(), startTime: Date() - ) + )! let span2 = storage.upsertSpan( id: "id2", name: "a name 2", @@ -58,7 +58,7 @@ class EmbraceStorageTests: XCTestCase { type: .performance, data: Data(), startTime: Date() - ) + )! // when fetching all records let records: [SpanRecord] = storage.fetchAll() diff --git a/Tests/EmbraceStorageInternalTests/FetchMethods/EmbraceStorage+SpanForSessionRecordTests.swift b/Tests/EmbraceStorageInternalTests/FetchMethods/EmbraceStorage+SpanForSessionRecordTests.swift index 0a584467..d0a7c5e8 100644 --- a/Tests/EmbraceStorageInternalTests/FetchMethods/EmbraceStorage+SpanForSessionRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/FetchMethods/EmbraceStorage+SpanForSessionRecordTests.swift @@ -42,8 +42,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { data: Data(), startTime: startTime, endTime: endTime, - processIdentifier: processIdentifier - ) + processId: processIdentifier + )! } func sessionRecord( @@ -65,7 +65,7 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { endTime: endTime, lastHeartbeatTime: lastHeartBeat ?? startTime, coldStart: coldStart - ) + )! } // MARK: Tests diff --git a/Tests/EmbraceStorageInternalTests/SpanRecordTests.swift b/Tests/EmbraceStorageInternalTests/SpanRecordTests.swift index 4d0f31d4..f8d59e33 100644 --- a/Tests/EmbraceStorageInternalTests/SpanRecordTests.swift +++ b/Tests/EmbraceStorageInternalTests/SpanRecordTests.swift @@ -156,7 +156,7 @@ class SpanRecordTests: XCTestCase { type: .performance, data: Data(), startTime: Date(timeIntervalSince1970: 1), - processIdentifier: TestConstants.processId + processId: TestConstants.processId ) storage.upsertSpan( id: "id3", diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift index e36c715f..330d461c 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests+ClearDataDate.swift @@ -23,7 +23,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 1), attemptCount: 0, date: Date(timeInterval: -1300, since: now) - ) + )! let record2 = UploadDataRecord.create( context: cache.coreData.context, id: "id2", @@ -31,7 +31,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate - ) + )! let record3 = UploadDataRecord.create( context: cache.coreData.context, id: "id3", @@ -39,7 +39,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate - ) + )! let record4 = UploadDataRecord.create( context: cache.coreData.context, id: "id4", @@ -47,7 +47,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 300), attemptCount: 0, date: Date(timeInterval: -1200, since: now) - ) + )! let record5 = UploadDataRecord.create( context: cache.coreData.context, id: "id5", @@ -55,7 +55,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 400), attemptCount: 0, date: Date(timeInterval: -1100, since: now) - ) + )! let record6 = UploadDataRecord.create( context: cache.coreData.context, id: "id6", @@ -63,7 +63,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 100), attemptCount: 0, date: Date(timeInterval: -1000, since: now) - ) + )! cache.coreData.context.performAndWait { do { @@ -109,35 +109,35 @@ extension EmbraceUploadCacheTests { type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: Date(timeInterval: -1300, since: now) - ) + )! let record2 = UploadDataRecord.create( context: cache.coreData.context, id: "id2", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate - ) + )! let record3 = UploadDataRecord.create( context: cache.coreData.context, id: "id3", type: 0, data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate - ) + )! let record4 = UploadDataRecord.create( context: cache.coreData.context, id: "id4", type: 0, data: Data(repeating: 3, count: 300), attemptCount: 0, date: Date(timeInterval: -1200, since: now) - ) + )! let record5 = UploadDataRecord.create( context: cache.coreData.context, id: "id5", type: 0, data: Data(repeating: 3, count: 400), attemptCount: 0, date: Date(timeInterval: -1100, since: now) - ) + )! let record6 = UploadDataRecord.create( context: cache.coreData.context, id: "id6", @@ -145,7 +145,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 100), attemptCount: 0, date: Date(timeInterval: -1000, since: now) - ) + )! cache.coreData.context.performAndWait { do { @@ -194,7 +194,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 1), attemptCount: 0, date: Date(timeInterval: -1300, since: now) - ) + )! let record2 = UploadDataRecord.create( context: cache.coreData.context, id: "id2", @@ -202,7 +202,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate - ) + )! let record3 = UploadDataRecord.create( context: cache.coreData.context, id: "id3", @@ -210,7 +210,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 1), attemptCount: 0, date: oldDate - ) + )! let record4 = UploadDataRecord.create( context: cache.coreData.context, id: "id4", @@ -218,7 +218,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 300), attemptCount: 0, date: Date(timeInterval: -1200, since: now) - ) + )! let record5 = UploadDataRecord.create( context: cache.coreData.context, id: "id5", @@ -226,7 +226,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 400), attemptCount: 0, date: Date(timeInterval: -1100, since: now) - ) + )! let record6 = UploadDataRecord.create( context: cache.coreData.context, id: "id6", @@ -234,7 +234,7 @@ extension EmbraceUploadCacheTests { data: Data(repeating: 3, count: 100), attemptCount: 0, date: Date(timeInterval: -1000, since: now) - ) + )! cache.coreData.context.performAndWait { do { diff --git a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift index 153d0946..d94ec1d5 100644 --- a/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift +++ b/Tests/EmbraceUploadInternalTests/EmbraceUploadCacheTests.swift @@ -57,7 +57,7 @@ class EmbraceUploadCacheTests: XCTestCase { data: Data(), attemptCount: 0, date: Date() - ) + )! let data2 = UploadDataRecord.create( context: cache.coreData.context, id: "id2", @@ -65,7 +65,7 @@ class EmbraceUploadCacheTests: XCTestCase { data: Data(), attemptCount: 0, date: Date() - ) + )! let data3 = UploadDataRecord.create( context: cache.coreData.context, id: "id3", @@ -73,7 +73,7 @@ class EmbraceUploadCacheTests: XCTestCase { data: Data(), attemptCount: 0, date: Date() - ) + )! cache.coreData.save() diff --git a/Tests/TestSupport/Extensions/EmbraceStorage+Extension.swift b/Tests/TestSupport/Extensions/EmbraceStorage+Extension.swift index 25db8441..584e65b9 100644 --- a/Tests/TestSupport/Extensions/EmbraceStorage+Extension.swift +++ b/Tests/TestSupport/Extensions/EmbraceStorage+Extension.swift @@ -7,7 +7,7 @@ import EmbraceCommonInternal @testable import EmbraceStorageInternal public extension EmbraceStorage { - static func createInMemoryDb(runMigrations: Bool = true) throws -> EmbraceStorage { + static func createInMemoryDb() throws -> EmbraceStorage { let storage = try EmbraceStorage( options: .init(storageMechanism: .inMemory(name: UUID().uuidString)), logger: MockLogger() From 8be8b2444de7ba26d59da8caf553f31d0099c543 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Thu, 6 Feb 2025 14:33:46 -0300 Subject: [PATCH 14/17] Fixing predicates --- .../Records/EmbraceStorage+Span.swift | 14 ++++++++++---- .../Internal/StorageSpanExporterTests.swift | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index 4f836773..e24aee7d 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -35,6 +35,12 @@ extension EmbraceStorage { // update existing? if let span = fetchSpan(id: id, traceId: traceId) { + + // prevent modifications on closed spans! + guard span.endTime == nil else { + return span + } + span.name = name span.typeRaw = type.rawValue span.data = data @@ -76,7 +82,7 @@ extension EmbraceStorage { public func fetchSpan(id: String, traceId: String) -> SpanRecord? { let request = SpanRecord.createFetchRequest() request.fetchLimit = 1 - request.predicate = NSPredicate(format: "id == %@ AND traceId == %i", id, traceId) + request.predicate = NSPredicate(format: "id == %@ AND traceId == %@", id, traceId) return coreData.fetch(withRequest: request).first } @@ -88,9 +94,9 @@ extension EmbraceStorage { let request = SpanRecord.createFetchRequest() if let date = date { - request.predicate = NSPredicate(format: "date != nil AND date < %@", date as NSDate) + request.predicate = NSPredicate(format: "endTime != nil AND endTime < %@", date as NSDate) } else { - request.predicate = NSPredicate(format: "date != nil") + request.predicate = NSPredicate(format: "endTime != nil") } let spans = coreData.fetch(withRequest: request) @@ -102,7 +108,7 @@ extension EmbraceStorage { /// - endTime: Identifier of the trace containing this span public func closeOpenSpans(endTime: Date) { let request = SpanRecord.createFetchRequest() - request.predicate = NSPredicate(format: "date = nil") + request.predicate = NSPredicate(format: "endTime = nil") let spans = coreData.fetch(withRequest: request) diff --git a/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift b/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift index 00af01ff..6c6d6505 100644 --- a/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift +++ b/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift @@ -52,6 +52,7 @@ final class StorageSpanExporterTests: XCTestCase { let exportedSpan = try XCTUnwrap(exportedSpans.first) XCTAssertEqual(exportedSpan.traceId, traceId.hexString) XCTAssertEqual(exportedSpan.id, spanId.hexString) + XCTAssertEqual(exportedSpan.startTime.timeIntervalSince1970, startTime.timeIntervalSince1970, accuracy: 0.01) XCTAssertEqual(exportedSpan.endTime!.timeIntervalSince1970, endTime.timeIntervalSince1970, accuracy: 0.01) } From 133cf4153dfb6f80e266e76091825d84508697c9 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Thu, 6 Feb 2025 14:43:18 -0300 Subject: [PATCH 15/17] Fixing fetch spans ignoring session spans --- .../Records/EmbraceStorage+Span.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index e24aee7d..ff75f0d0 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -136,10 +136,12 @@ extension EmbraceStorage { let endTime = (session.endTime ?? session.lastHeartbeatTime) as NSDate + var predicate: NSPredicate + // special case for cold start sessions // we grab spans that might have started before the session but within the same process if session.coldStart { - request.predicate = NSPredicate( + predicate = NSPredicate( format: "processIdRaw == %@ AND startTime <= %@", session.processIdRaw, endTime @@ -169,7 +171,15 @@ extension EmbraceStorage { endTime ) - request.predicate = NSCompoundPredicate(type: .or, subpredicates: [predicate1, predicate2]) + predicate = NSCompoundPredicate(type: .or, subpredicates: [predicate1, predicate2]) + } + + // ignore session spans? + if ignoreSessionSpans { + let sessionTypePredicate = NSPredicate(format: "typeRaw != %@", SpanType.session.rawValue) + request.predicate = NSCompoundPredicate(type: .and, subpredicates: [sessionTypePredicate, predicate]) + } else { + request.predicate = predicate } return coreData.fetch(withRequest: request) From 0fef0e4c7ac21324d0077a1cbff90581279a0eab Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Thu, 6 Feb 2025 15:40:29 -0300 Subject: [PATCH 16/17] Fixing tests --- .../Records/EmbraceStorage+Span.swift | 15 +++++---------- .../Session/UnsentDataHandlerTests.swift | 9 +++++---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index ff75f0d0..ff25e40c 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -152,23 +152,18 @@ extension EmbraceStorage { else { let startTime = session.startTime as NSDate - // span starts within session and - // - ends before session ends or - // - hasn't ended yet + // span starts within session let predicate1 = NSPredicate( - format: "startTime >= %@ AND (endTime = nil OR endTime <= %@)", + format: "startTime >= %@ AND startTime <= %@", startTime, endTime ) - // span starts before session and - // - ends within session or - // - hasn't ended yet + // span starts before session and doesn't end before session starts let predicate2 = NSPredicate( - format: "startTime < %@ AND (endTime = nil OR (endTime >= %@ AND endTime <= %@))", + format: "startTime < %@ AND (endTime = nil OR endTime >= %@)", startTime, - startTime, - endTime + startTime ) predicate = NSCompoundPredicate(type: .or, subpredicates: [predicate1, predicate2]) diff --git a/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift b/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift index 8d0b7d78..97fe5832 100644 --- a/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Session/UnsentDataHandlerTests.swift @@ -173,10 +173,7 @@ class UnsentDataHandlerTests: XCTestCase { endTime: Date() ) - // when sending unsent sessions - UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, crashReporter: crashReporter) - - // then the crash report id is set on the session + // the crash report id is set on the session let listener = CoreDataListener() let expectation1 = XCTestExpectation() listener.onUpdatedObjects = { records in @@ -185,6 +182,10 @@ class UnsentDataHandlerTests: XCTestCase { expectation1.fulfill() } } + + // when sending unsent sessions + UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, crashReporter: crashReporter) + wait(for: [expectation1], timeout: .veryLongTimeout) // then a crash report was sent From 3a9b43ad46ca573b45ba9bf5f3540b533aa164e1 Mon Sep 17 00:00:00 2001 From: Ignacio Tischelman Date: Thu, 6 Feb 2025 16:08:19 -0300 Subject: [PATCH 17/17] Fixing close open spans --- Package.resolved | 12 ++++++------ .../Records/EmbraceStorage+Span.swift | 7 +++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Package.resolved b/Package.resolved index a0639290..9f106117 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift.git", "state" : { - "revision" : "6a90b7e77e29f9bda6c2b3a4165a40d6c02cfda1", - "version" : "1.23.0" + "revision" : "8c5e99d0255c373e0330730d191a3423c57373fb", + "version" : "1.24.2" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "05c36b57453d23ea63785d58a7dbc7b70ba1745e", - "version" : "1.23.0" + "revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6", + "version" : "1.24.1" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "d57a5aecf24a25b32ec4a74be2f5d0a995a47c4b", - "version" : "1.27.0" + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" } }, { diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift index ff25e40c..84245368 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -103,12 +103,15 @@ extension EmbraceStorage { coreData.deleteRecords(spans) } - /// Synchronously closes all open spans with the given `endTime`. + /// Synchronously closes all open spans from previous processes with the given `endTime`. /// - Parameters: /// - endTime: Identifier of the trace containing this span public func closeOpenSpans(endTime: Date) { let request = SpanRecord.createFetchRequest() - request.predicate = NSPredicate(format: "endTime = nil") + request.predicate = NSPredicate( + format: "endTime = nil AND processIdRaw != %@", + ProcessIdentifier.current.hex + ) let spans = coreData.fetch(withRequest: request)