diff --git a/EmbraceIO.podspec b/EmbraceIO.podspec index ce8498bd..1293f942 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 17d5ee72..602f740a 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 d709f915..0631f13d 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.13.0" - ), - .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/") @@ -101,7 +94,10 @@ let package = Package( // common -------------------------------------------------------------------- .target( - name: "EmbraceCommonInternal" + name: "EmbraceCommonInternal", + dependencies: [ + .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift") + ] ), .testTarget( name: "EmbraceCommonInternalTests", @@ -190,8 +186,8 @@ let package = Package( name: "EmbraceStorageInternal", dependencies: [ "EmbraceCommonInternal", - "EmbraceSemantics", - .product(name: "GRDB", package: "GRDB.swift") + "EmbraceCoreDataInternal", + "EmbraceSemantics" ] ), .testTarget( @@ -208,8 +204,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/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 6c800b97..b1fb3989 100644 --- a/Sources/EmbraceCore/Capture/CaptureServices.swift +++ b/Sources/EmbraceCore/Capture/CaptureServices.swift @@ -103,8 +103,8 @@ final class CaptureServices { } @objc func onSessionStart(notification: Notification) { - if let session = notification.object as? SessionRecord { - crashReporter?.currentSessionId = session.id.toString + 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/Capture/ResourceCaptureService.swift b/Sources/EmbraceCore/Capture/ResourceCaptureService.swift index 997053c4..eefaf115 100644 --- a/Sources/EmbraceCore/Capture/ResourceCaptureService.swift +++ b/Sources/EmbraceCore/Capture/ResourceCaptureService.swift @@ -21,18 +21,12 @@ class ResourceCaptureService: CaptureService { 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 - ) - ) - } 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/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/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/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift b/Sources/EmbraceCore/Internal/Identifiers/DeviceIdentifier+Persistence.swift index a608277e..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 = resource.uuidValue { - 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/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 fcbd1e33..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]) { @@ -69,20 +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 - } - - if let value = record.stringValue { - let key = String(format: LogSemantics.keyPropertiesPrefix, record.key) - attributes[key] = 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/Exporter/DefaultLogBatcher.swift b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift index 3637d1c5..1f0865e7 100644 --- a/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift +++ b/Sources/EmbraceCore/Internal/Logs/Exporter/DefaultLogBatcher.swift @@ -3,17 +3,18 @@ // import Foundation - import EmbraceStorageInternal import EmbraceCommonInternal +import OpenTelemetryApi +import OpenTelemetrySdk protocol LogBatcherDelegate: AnyObject { - func batchFinished(withLogs logs: [LogRecord]) + func batchFinished(withLogs logs: [EmbraceLog]) } protocol LogBatcher { - func addLogRecord(logRecord: LogRecord) - func renewBatch(withLogs logRecords: [LogRecord]) + func addLogRecord(logRecord: ReadableLogRecord) + func renewBatch(withLogs logRecords: [EmbraceLog]) func forceEndCurrentBatch() } @@ -38,15 +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) - } + 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) } } } @@ -59,23 +62,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/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 edf46571..05e591be 100644 --- a/Sources/EmbraceCore/Internal/Logs/LogController.swift +++ b/Sources/EmbraceCore/Internal/Logs/LogController.swift @@ -53,14 +53,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: [EmbraceLog] = storage.fetchAll(excludingProcessIdentifier: .current) + if logs.count > 0 { + send(batches: divideInBatches(logs)) } } @@ -141,7 +137,7 @@ class LogController: LogControllable { } extension LogController { - func batchFinished(withLogs logs: [LogRecord]) { + func batchFinished(withLogs logs: [EmbraceLog]) { guard sdkStateProvider?.isEnabled == true else { return } @@ -175,6 +171,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. @@ -185,12 +185,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) @@ -206,7 +204,7 @@ private extension LogController { } func send( - logs: [LogRecord], + logs: [EmbraceLog], resourcePayload: ResourcePayload, metadataPayload: MetadataPayload ) { @@ -228,18 +226,18 @@ private extension LogController { return } - try? self.storage?.remove(logs: logs) + self.storage?.remove(logs: logs) } } catch let exception { Error.couldntCreatePayload(reason: exception.localizedDescription).log() } } - 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 { @@ -266,12 +264,12 @@ private extension LogController { throw Error.couldntAccessStorageModule } - var resources: [MetadataRecord] = [] + var resources: [EmbraceMetadata] = [] 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) @@ -284,15 +282,15 @@ private extension LogController { throw Error.couldntAccessStorageModule } - var metadata: [MetadataRecord] = [] + var metadata: [EmbraceMetadata] = [] 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 d6b46d17..d3c56621 100644 --- a/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift +++ b/Sources/EmbraceCore/Internal/ResourceStorageExporter/ResourceStorageExporter.swift @@ -30,12 +30,10 @@ 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] = 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..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 @@ -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..c663a96e 100644 --- a/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/LogPayloadBuilder.swift @@ -8,12 +8,12 @@ import EmbraceCommonInternal 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) + static func build(log: EmbraceLog) -> LogPayload { + var finalAttributes: [Attribute] = log.allAttributes().map { entry in + Attribute(key: entry.key, value: entry.valueRaw) } - 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, @@ -32,24 +32,20 @@ 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 { - 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 95f1a47e..68d95f5e 100644 --- a/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SessionPayloadBuilder.swift @@ -10,63 +10,46 @@ class SessionPayloadBuilder { static var resourceName = "emb.session.upload_index" - class func build(for sessionRecord: SessionRecord, storage: EmbraceStorage) -> PayloadEnvelope<[SpanPayload]> { - var resource: MetadataRecord? - - do { - // fetch resource - resource = try storage.fetchRequiredPermanentResource(key: resourceName) - } catch { - Embrace.logger.debug("Error fetching \(resourceName) resource!") + class func build(for session: EmbraceSession, storage: EmbraceStorage) -> PayloadEnvelope<[SpanPayload]>? { + guard let sessionId = session.id else { + return nil } // increment counter or create resource if needed + var resource = storage.fetchRequiredPermanentResource(key: resourceName) var counter: Int = -1 - do { - if var resource = resource { - counter = (resource.integerValue ?? 0) + 1 - resource.value = .string(String(counter)) - try storage.updateMetadata(resource) - } 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 let (spans, spanSnapshots) = SpansPayloadBuilder.build( - for: sessionRecord, + for: session, storage: storage, sessionNumber: counter ) // 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: [EmbraceMetadata] = 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)") - } + var metadata: [EmbraceMetadata] = [] + 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..d06a963a 100644 --- a/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift +++ b/Sources/EmbraceCore/Payload/Builders/SpansPayloadBuilder.swift @@ -13,22 +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 - var records: [SpanRecord] = [] + let endTime = session.endTime ?? session.lastHeartbeatTime // 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: session, ignoreSessionSpans: true, limit: spanCountLimit) // decode spans and separate them by closed/open var spans: [SpanPayload] = [] @@ -36,7 +30,7 @@ class SpansPayloadBuilder { // fetch and add session span first if let sessionSpanPayload = buildSessionSpanPayload( - for: sessionRecord, + for: session, storage: storage, sessionNumber: sessionNumber ) { @@ -51,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) @@ -70,29 +64,32 @@ class SpansPayloadBuilder { } class func buildSessionSpanPayload( - for sessionRecord: SessionRecord, + for session: EmbraceSession, storage: EmbraceStorage, sessionNumber: Int ) -> SpanPayload? { do { var spanData: SpanData? - let sessionSpan = try 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) } - let properties = try storage.fetchCustomPropertiesForSessionId(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.id):\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 d62edbe5..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,25 +19,25 @@ 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 { 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..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 @@ -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/Payload/Utils/PayloadUtils.swift b/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift index 2289a75e..c2a29c68 100644 --- a/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift +++ b/Sources/EmbraceCore/Payload/Utils/PayloadUtils.swift @@ -10,37 +10,25 @@ class PayloadUtils { static func fetchResources( from fetcher: EmbraceStorageMetadataFetcher, sessionId: SessionIdentifier? - ) -> [MetadataRecord] { + ) -> [EmbraceMetadata] { guard let sessionId = sessionId else { 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( from fetcher: EmbraceStorageMetadataFetcher, sessionId: SessionIdentifier? - ) -> [MetadataRecord] { + ) -> [EmbraceMetadata] { guard let sessionId = sessionId else { 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..00dc1805 100644 --- a/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift +++ b/Sources/EmbraceCore/Public/Metadata/MetadataHandler+Personas.swift @@ -14,15 +14,11 @@ extension MetadataHandler { return [] } - 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)") + var records: [EmbraceMetadata] = [] + 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+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..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.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 } ) @@ -213,7 +215,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 @@ -246,27 +248,16 @@ extension MetadataHandler { 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) + for record in oldRecords { + guard let type = MetadataRecordType(rawValue: record.type), + let lifespan = MetadataRecordLifespan(rawValue: record.lifespan) else { + continue } - 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)") + storage.addMetadata(key: record.key, value: record.value, type: type, lifespan: lifespan) } + + coreData.destroy() } } 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..c927db1d 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? + var session: EmbraceSession? 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 var 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, @@ -97,7 +99,7 @@ class UnsentDataHandler { static public func sendCrashLog( report: CrashReport, reporter: CrashReporter?, - session: SessionRecord?, + session: EmbraceSession?, storage: EmbraceStorage?, upload: EmbraceUpload?, otel: EmbraceOpenTelemetry? @@ -152,7 +154,7 @@ class UnsentDataHandler { otel: EmbraceOpenTelemetry?, storage: EmbraceStorage?, report: CrashReport, - session: SessionRecord?, + session: EmbraceSession?, timestamp: Date ) -> [String: String] { @@ -194,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 @@ -217,7 +213,7 @@ class UnsentDataHandler { } static public func sendSession( - _ session: SessionRecord, + _ session: EmbraceSession, storage: EmbraceStorage, upload: EmbraceUpload, performCleanUp: Bool = true @@ -229,7 +225,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,64 +234,48 @@ 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) - - if performCleanUp { - cleanOldSpans(storage: storage) - cleanMetadata(storage: storage) - } + // remove session from storage + // we can remove this immediately because the upload module will cache it until the upload succeeds + if let record = session as? SessionRecord { + storage.delete(record) + } - } 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/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 b1d7cd6a..dd5e751b 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() @@ -96,6 +96,10 @@ class SessionController: SessionControllable { return nil } + guard let storage = storage else { + return nil + } + // detect cold start let isColdStart = firstSession @@ -125,15 +129,15 @@ class SessionController: SessionControllable { currentSessionSpan = span // create session record - var session = SessionRecord( + 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 ) - session.coldStart = isColdStart + session?.coldStart = isColdStart currentSession = session // save session record @@ -239,28 +243,16 @@ 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)") + if let record = session as? SessionRecord { + storage?.delete(record) } currentSession = nil diff --git a/Sources/EmbraceCore/Session/SessionSpanUtils.swift b/Sources/EmbraceCore/Session/SessionSpanUtils.swift index d83e89a4..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 @@ -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/EmbraceCoreDataInternal/CoreDataWrapper.swift b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift index 8370c37c..d3ee7c8f 100644 --- a/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift +++ b/Sources/EmbraceCoreDataInternal/CoreDataWrapper.swift @@ -10,10 +10,11 @@ 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 + let lock = NSLock() public init(options: CoreDataWrapper.Options, logger: InternalLogger) throws { self.options = options @@ -36,6 +37,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,42 +52,101 @@ public class CoreDataWrapper { self.context.persistentStoreCoordinator = self.container.persistentStoreCoordinator } - /// Asynchronously saves all changes on the current context to disk + /// Removes the database file + /// - Note: Only used in tests!!! + public func destroy() { +#if canImport(XCTest) + 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 + } + + 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 + } + } +#endif + } + + /// 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 } - /// 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 { + return lock.withLock { + + var result: Int = 0 + context.performAndWait { + do { + result = try context.count(for: request) + } catch { } + } + return result + } + } + + /// 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 + /// 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/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..1af90aab 100644 --- a/Sources/EmbraceStorageInternal/EmbraceStorage.swift +++ b/Sources/EmbraceStorageInternal/EmbraceStorage.swift @@ -4,16 +4,17 @@ import Foundation import EmbraceCommonInternal -import GRDB +import EmbraceCoreDataInternal +import CoreData 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. +/// It provides an abstraction layer over a CoreData 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 +23,51 @@ 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 + var entities: [NSEntityDescription] = [ + SessionRecord.entityDescription, + SpanRecord.entityDescription, + MetadataRecord.entityDescription, + ] + entities.append(contentsOf: LogRecord.entityDescriptions) + + let coreDataOptions = CoreDataWrapper.Options( + storageMechanism: options.storageMechanism, + entities: entities + ) + 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) - } - } - - /// 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) + /// - Parameter record: `NSManagedObject` to delete + public func delete(_ record: T) { + coreData.deleteRecord(record) } - /// 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) + /// 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 asynchronously. - /// - Parameter completion: Completion block called with an array `[T]` with the fetch result on success, or an `Error` on failure + /// Fetches all the records of the given type in the storage synchronously. /// - 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() -> [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 5777c63a..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 9672f743..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 e0b6fb98..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 ad55b0ec..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 78890f30..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 9ee1887d..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 54668db9..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 5c80da7d..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/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+Log.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift new file mode 100644 index 00000000..9b94718e --- /dev/null +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Log.swift @@ -0,0 +1,66 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import OpenTelemetryApi + +public protocol LogRepository { + func createLog( + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date, + attributes: [String: AttributeValue] + ) -> EmbraceLog? + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] + func remove(logs: [EmbraceLog]) + func removeAllLogs() +} + +extension EmbraceStorage { + + @discardableResult + public func createLog( + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date = Date(), + attributes: [String: OpenTelemetryApi.AttributeValue] + ) -> EmbraceLog? { + if let log = LogRecord.create( + context: coreData.context, + id: id, + processId: processId, + severity: severity, + body: body, + timestamp: timestamp, + attributes: attributes + ) { + coreData.save() + return log + } + + return nil + } + + public func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] { + let request = LogRecord.createFetchRequest() + request.predicate = NSPredicate(format: "processIdRaw != %@", processIdentifier.hex) + + return coreData.fetch(withRequest: request) + } + + public func removeAllLogs() { + let logs: [LogRecord] = fetchAll() + remove(logs: 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 3e9fe3e1..8b3af64e 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Metadata.swift @@ -3,16 +3,15 @@ // import Foundation -import GRDB 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() -> [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 { @@ -26,122 +25,106 @@ extension EmbraceStorage { type: MetadataRecordType, lifespan: MetadataRecordLifespan, lifespanId: String = "" - ) throws -> MetadataRecord? { + ) -> MetadataRecord? { - let metadata = MetadataRecord( + // update existing? + if let metadata = updateMetadata( key: key, - value: .string(value), + value: value, type: type, lifespan: lifespan, lifespanId: lifespanId - ) + ) { + return metadata + } - if try addMetadata(metadata) { + // create new + guard shouldAddMetadata(type: type, lifespanId: lifespanId) else { + return nil + } + + if let metadata = MetadataRecord.create( + context: coreData.context, + key: key, + value: value, + type: type, + lifespan: lifespan, + lifespanId: lifespanId + ) { + coreData.save() 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 - } - } + /// 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 + ) - 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 - } + 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 + @discardableResult public func updateMetadata( key: String, value: String, type: MetadataRecordType, lifespan: MetadataRecordLifespan, lifespanId: String - ) throws { - - 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) - } - } + ) -> MetadataRecord? { - /// Updates the given `MetadataRecord`. - public func updateMetadata(_ record: MetadataRecord) throws { - try dbQueue.write { db in - try record.update(db) + guard let metadata = fetchMetadata(key: key, type: type, lifespan: lifespan, lifespanId: lifespanId) else { + return nil } + + 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. @@ -150,208 +133,268 @@ extension EmbraceStorage { type: MetadataRecordType, 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? { - - 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) - } + request.predicate = NSCompoundPredicate(type: .and, subpredicates: [typePredicate, keyPredicate]) + + 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) + public func fetchRequiredPermanentResource(key: String) -> MetadataRecord? { + 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) - } + public func fetchAllResources() -> [EmbraceMetadata] { + 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) + public func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { + + 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) - } + public func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { + + 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) + public func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { + 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) + public func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { + 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) - } + public func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { + + 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..fd426ce3 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Session.swift @@ -4,14 +4,13 @@ import Foundation import EmbraceCommonInternal -import GRDB 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 @@ -22,80 +21,95 @@ 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 - ) throws -> SessionRecord { - let session = SessionRecord( + crashReportId: String? = nil, + coldStart: Bool = false, + cleanExit: Bool = false, + appTerminated: Bool = false + ) -> 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 + session.coldStart = coldStart + session.cleanExit = cleanExit + session.appTerminated = appTerminated + + if let lastHeartbeatTime = lastHeartbeatTime { + session.lastHeartbeatTime = lastHeartbeatTime + } + + coreData.save() + return session + } + + // create new + if let session = SessionRecord.create( + context: coreData.context, id: id, - state: state, processId: processId, + state: state, traceId: traceId, spanId: spanId, startTime: startTime, endTime: endTime, - lastHeartbeatTime: lastHeartbeatTime - ) - - try upsertSession(session) - - 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) + lastHeartbeatTime: lastHeartbeatTime, + coldStart: coldStart, + cleanExit: cleanExit, + appTerminated: appTerminated + ) { + coreData.save() + return session } + + return nil } /// 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 - - var filter = SessionRecord.order(SessionRecord.Schema.startTime.desc) + public func fetchLatestSession(ignoringCurrentSessionId sessionId: SessionIdentifier? = nil) -> SessionRecord? { + let request = SessionRecord.createFetchRequest() + request.fetchLimit = 1 + request.sortDescriptors = [NSSortDescriptor(key: "startTime", ascending: false)] - 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..84245368 100644 --- a/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorage+Span.swift @@ -5,13 +5,13 @@ import Foundation import EmbraceCommonInternal import EmbraceSemantics -import GRDB +import CoreData 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 +22,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, @@ -30,10 +30,34 @@ extension EmbraceStorage { data: Data, startTime: Date, endTime: Date? = nil, - processIdentifier: ProcessIdentifier = .current - ) throws -> SpanRecord { + processId: ProcessIdentifier = .current + ) -> SpanRecord? { + + // 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 + span.startTime = startTime + span.endTime = endTime + span.processIdRaw = processId.hex + + coreData.save() + return span + } - let span = SpanRecord( + // make space if needed + removeOldSpanIfNeeded(forType: type) + + // add new + if let span = SpanRecord.create( + context: coreData.context, id: id, name: name, traceId: traceId, @@ -41,26 +65,13 @@ extension EmbraceStorage { data: data, startTime: startTime, endTime: endTime, - processIdentifier: processIdentifier - ) - try upsertSpan(span) - - return span - } - - /// 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]" - ) + processId: processId + ) { + coreData.save() + return span } + + return nil } /// Fetches the stored `SpanRecord` synchronously with the given identifiers, if any. @@ -68,173 +79,128 @@ 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 == %@", 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: "endTime != nil AND endTime < %@", date as NSDate) + } else { + request.predicate = NSPredicate(format: "endTime != nil") } + + let spans = coreData.fetch(withRequest: request) + 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) 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: "endTime = nil AND processIdRaw != %@", + ProcessIdentifier.current.hex + ) + + let spans = coreData.fetch(withRequest: request) + + for span in spans { + span.endTime = endTime } + + coreData.save() } /// Fetch spans for the given session record /// 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 - ) throws -> [SpanRecord] { - return try dbQueue.read { db in - var query = SpanRecord.filter(for: sessionRecord) - - if ignoreSessionSpans { - query = query.filter(SpanRecord.Schema.type != SpanType.session) - } + ) -> [SpanRecord] { - return try query - .limit(limit) - .fetchAll(db) - } - } -} + let request = SpanRecord.createFetchRequest() + request.fetchLimit = limit -// 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 - } + let endTime = (session.endTime ?? session.lastHeartbeatTime) as NSDate - // check limit and delete if necessary - // default to 1500 if limit is not set - let limit = options.spanLimits[span.type, default: Self.defaultSpanLimitByType] + var predicate: NSPredicate - 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 session.coldStart { + predicate = NSPredicate( + format: "processIdRaw == %@ AND startTime <= %@", + session.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) - } + // otherwise we check if the span is within the boundaries of the session + else { + let startTime = session.startTime as NSDate - func spanCount(db: Database, type: SpanType) throws -> Int { - return try requestSpans(of: type) - .fetchCount(db) - } - - func fetchSpans(db: Database, type: SpanType, limit: Int?) throws -> [SpanRecord] { - var request = requestSpans(of: type) - .order(SpanRecord.Schema.startTime) - - if let limit = limit { - request = request.limit(limit) - } - - return try request.fetchAll(db) - } + // span starts within session + let predicate1 = NSPredicate( + format: "startTime >= %@ AND startTime <= %@", + startTime, + endTime + ) - 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) - ) + // span starts before session and doesn't end before session starts + let predicate2 = NSPredicate( + format: "startTime < %@ AND (endTime = nil OR endTime >= %@)", + startTime, + 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) + predicate = NSCompoundPredicate(type: .or, subpredicates: [predicate1, predicate2]) } - // if ignoring session span - // select where type is not session span - if ignoreSessionSpans == true { - filter = filter.filter(SpanRecord.Schema.type != SpanType.session.rawValue) + // 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 filter + return coreData.fetch(withRequest: request) } +} - func fetchSpans( - db: Database, - startTime: Date, - endTime: Date, - includeOlder: Bool, - ignoreSessionSpans: Bool, - limit: Int? - ) throws -> [SpanRecord] { +// 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] - var request = spanInTimeFrameByTypeRequest( - startTime: startTime, - endTime: endTime, - includeOlder: includeOlder, - ignoreSessionSpans: ignoreSessionSpans - ).order(SpanRecord.Schema.startTime) + let request = SpanRecord.createFetchRequest() + request.predicate = NSPredicate(format: "typeRaw == %@", type.rawValue) + let count = coreData.count(withRequest: request) - if let limit = limit { - request = request.limit(limit) - } + if count >= limit { + request.fetchLimit = count - limit + 1 + request.sortDescriptors = [ NSSortDescriptor(key: "startTime", ascending: true) ] - return try request.fetchAll(db) + let spansToDelete = coreData.fetch(withRequest: request) + coreData.deleteRecords(spansToDelete) + } } } diff --git a/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift b/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift new file mode 100644 index 00000000..1e8c3c55 --- /dev/null +++ b/Sources/EmbraceStorageInternal/Records/EmbraceStorageRecord.swift @@ -0,0 +1,10 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import CoreData + +public protocol EmbraceStorageRecord: NSManagedObject { + static var entityName: String { 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..0a87b248 --- /dev/null +++ b/Sources/EmbraceStorageInternal/Records/LogAttributeRecord.swift @@ -0,0 +1,37 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import CoreData +import EmbraceCommonInternal +import OpenTelemetryApi + +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 static func create( + context: NSManagedObjectContext, + key: String, + value: AttributeValue, + log: LogRecord + ) -> 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 + + return record + } +} + +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..506be9f8 --- /dev/null +++ b/Sources/EmbraceStorageInternal/Records/LogRecord.swift @@ -0,0 +1,160 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal +import OpenTelemetryApi +import CoreData + +public class LogRecord: NSManagedObject, EmbraceLog { + @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] + + static func create( + context: NSManagedObjectContext, + id: LogIdentifier, + processId: ProcessIdentifier, + severity: LogSeverity, + body: String, + timestamp: Date = Date(), + attributes: [String: AttributeValue] + ) -> 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 + record.body = body + record.timestamp = timestamp + + for (key, value) in attributes { + if 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) + } + + 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 var attribute = attribute(forKey: key) { + attribute.value = value + return + } + + guard let context = managedObjectContext else { + return + } + + if 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/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..eafacd9e 100644 --- a/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/MetadataRecord.swift @@ -4,82 +4,88 @@ import Foundation import EmbraceCommonInternal -import GRDB +import CoreData import OpenTelemetryApi -public enum MetadataRecordType: String, Codable { - /// Resource that is attached to session and logs data - case resource +public class MetadataRecord: NSManagedObject, EmbraceMetadata { + @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 - /// 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 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 - - /// Main initializer for the MetadataRecord - public init( + 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? { + 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 + 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..5f02f445 100644 --- a/Sources/EmbraceStorageInternal/Records/SessionRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/SessionRecord.swift @@ -4,33 +4,34 @@ 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, EmbraceSession { + @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 static func create( + context: NSManagedObjectContext, id: SessionIdentifier, - state: SessionState, processId: ProcessIdentifier, + state: SessionState, traceId: String, spanId: String, startTime: Date, @@ -39,50 +40,105 @@ 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? { + 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 + 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 = "Session" - 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..a0bcbab3 100644 --- a/Sources/EmbraceStorageInternal/Records/SpanRecord.swift +++ b/Sources/EmbraceStorageInternal/Records/SpanRecord.swift @@ -4,20 +4,21 @@ 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, EmbraceSpan { + @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 + + class func create( + context: NSManagedObjectContext, id: String, name: String, traceId: String, @@ -25,46 +26,81 @@ 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 + 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 + record.typeRaw = type.rawValue + record.data = data + record.startTime = startTime + record.endTime = endTime + record.processIdRaw = processId.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/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift b/Sources/EmbraceUploadInternal/Cache/EmbraceUploadCache.swift index 66061901..e63200bb 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 } @@ -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/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..dba78160 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 @@ -22,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/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..eecd3c42 100644 --- a/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Capture/Network/NetworkPayloadCapture/NetworkPayloadCaptureHandlerTests.swift @@ -53,10 +53,10 @@ class NetworkPayloadCaptureHandlerTests: XCTestCase { handler.currentSessionId = nil // when a session starts - let session = SessionRecord( + let session = MockSession( 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..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() + let handler = try EmbraceStorage.createInMemoryDb() 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.createInMemoryDb() 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..85fc96bf 100644 --- a/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/DefaultInternalLoggerTests.swift @@ -12,16 +12,29 @@ 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() + storage = nil + } func test_none() { + let logger = DefaultInternalLogger() logger.level = .none @@ -32,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/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/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/DefaultLogBatcherTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/Exporter/DefaultLogBatcherTests.swift index 85321495..855a4c1f 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.5) 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..91bf50b6 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) @@ -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 c1bc73c8..9202fc8b 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! @@ -47,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) @@ -79,7 +73,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 +81,7 @@ class LogControllerTests: XCTestCase { givenStorage(withLogs: [log]) givenLogController() whenInvokingSetup() - try thenFetchesMetadataFromStorage(processId: log.processIdentifier) + try thenFetchesMetadataFromStorage(processId: log.processId!) } func testHavingLogsForLessThanABatch_onSetup_logUploaderShouldSendASingleBatch() { @@ -172,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() } @@ -295,30 +282,26 @@ private extension LogControllerTests { func givenSessionControllerWithSession() { sessionController = .init() - sessionController.currentSession = .init( + sessionController.currentSession = MockSession( id: .random, - state: .foreground, processId: .random, + state: .foreground, traceId: UUID().uuidString, spanId: UUID().uuidString, startTime: Date() ) } - func givenStorage(withLogs logs: [LogRecord] = []) { + func givenStorage(withLogs logs: [EmbraceLog] = []) { storage = .init() storage?.stubbedFetchAllExcludingProcessIdentifier = logs } - func givenStorageThatThrowsException() { - storage = .init(SpyStorage(shouldThrow: true)) - } - func whenInvokingSetup() { sut.uploadAllPersistedLogs() } - func whenInvokingBatchFinished(withLogs logs: [LogRecord]) { + func whenInvokingBatchFinished(withLogs logs: [EmbraceLog]) { sut.batchFinished(withLogs: logs) } @@ -374,10 +357,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 } } @@ -454,31 +440,25 @@ private extension LogControllerTests { } } - func randomLogRecord(sessionId: SessionIdentifier? = nil) -> LogRecord { + func randomLogRecord(sessionId: SessionIdentifier? = nil) -> EmbraceLog { - 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, + return MockLog( + id: .random, + processId: .random, severity: .info, body: UUID().uuidString, attributes: attributes ) } - func logsForMoreThanASingleBatch() -> [LogRecord] { + func logsForMoreThanASingleBatch() -> [EmbraceLog] { return (1...LogController.maxLogsPerBatch + 1).map { _ in randomLogRecord() } } } - -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..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,20 +100,19 @@ 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( - identifier: .init(), - processIdentifier: .random, + func randomLog(date: Date = Date()) -> EmbraceLog { + return MockLog( + 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..6c6d6505 100644 --- a/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift +++ b/Tests/EmbraceCoreTests/Internal/StorageSpanExporterTests.swift @@ -43,15 +43,16 @@ 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) 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) } @@ -87,10 +88,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..133c567f 100644 --- a/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/LogPayloadBuilderTests.swift @@ -7,15 +7,18 @@ 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, - 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) @@ -25,36 +28,37 @@ 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, - 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) 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 +66,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/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 f4af7d9f..25db215b 100644 --- a/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift +++ b/Tests/EmbraceCoreTests/Payload/PayloadUtilTests.swift @@ -7,14 +7,15 @@ import XCTest @testable import EmbraceStorageInternal @testable import EmbraceCommonInternal import OpenTelemetryApi +import TestSupport final class PayloadUtilTests: XCTestCase { func test_fetchResources() throws { // given - let mockResources: [MetadataRecord] = [ - .init( + let mockResources: [EmbraceMetadata] = [ + MockMetadata( key: "fake_res", - value: .string("fake_value"), + value: "fake_value", type: .requiredResource, lifespan: .process, lifespanId: ProcessIdentifier.current.hex @@ -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,10 +71,10 @@ 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: .string("fake_value"), + value: "fake_value", type: .customProperty, lifespan: .session, lifespanId: sessionId.toString @@ -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 1e705a35..e636e0dd 100644 --- a/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/SessionPayloadBuilderTests.swift @@ -11,15 +11,15 @@ 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, - 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..f3ec6386 100644 --- a/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Payload/SpansPayloadBuilderTests.swift @@ -14,15 +14,15 @@ 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, - 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,17 +70,15 @@ final class SpansPayloadBuilderTests: XCTestCase { endTime: spanData.hasEnded ? spanData.endTime : nil ) - try storage.upsertSpan(record) - return spanData } 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, - 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..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 @@ -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 @@ -320,3 +314,4 @@ final class EmbraceCoreTests: XCTestCase { return String((0.. SessionRecord { + func givenSessionRecord() -> MockSession { let endTime = Date(timeIntervalSince1970: 60) let heartbeat = Date(timeIntervalSince1970: 58) - return SessionRecord( + return MockSession( id: TestConstants.sessionId, - state: .foreground, processId: TestConstants.processId, + state: .foreground, traceId: TestConstants.traceId, spanId: TestConstants.spanId, startTime: TestConstants.date, @@ -324,7 +323,13 @@ private extension SessionSpanUtilsTests { appTerminated: .random()) } - func givenCustomProperty(withKey key: String, value: String, lifespan: MetadataRecordLifespan) -> MetadataRecord { - .init(key: key, value: .string(value), type: .customProperty, lifespan: lifespan, lifespanId: .random()) + func givenCustomProperty(withKey key: String, value: String, lifespan: MetadataRecordLifespan) -> MockMetadata { + MockMetadata( + 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..97fe5832 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,33 +163,30 @@ 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), 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() - 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() } } + + // when sending unsent sessions + UnsentDataHandler.sendUnsentData(storage: storage, upload: upload, otel: otel, crashReporter: crashReporter) + wait(for: [expectation1], timeout: .veryLongTimeout) - cancellable.cancel() // then a crash report was sent // then a session request was sent @@ -203,11 +199,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 +219,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 +227,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 +238,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 +262,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 +274,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 +303,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 +314,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 +338,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 +350,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 +370,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 +378,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 +388,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 +417,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 +440,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,14 +468,14 @@ class UnsentDataHandlerTests: XCTestCase { ) // given open span in storage - _ = try storage.addSpan( + storage.upsertSpan( id: TestConstants.spanId, name: "test", traceId: TestConstants.traceId, type: .performance, data: Data(), startTime: Date(timeIntervalSinceNow: -50), - processIdentifier: TestConstants.processId + processId: TestConstants.processId ) // when sending unsent sessions @@ -501,32 +484,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 +506,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 +567,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 +582,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 +613,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 +626,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 +667,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 +680,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 +693,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/TestDoubles/MockMetadataFetcher.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockMetadataFetcher.swift index 715d0c69..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() throws -> [MetadataRecord] { + func fetchAllResources() -> [EmbraceMetadata] { return metadata } - func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { return metadata.filter { record in (record.type == .resource || record.type == .requiredResource) } } - func fetchResourcesForProcessId(_ processId: ProcessIdentifier) throws -> [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) throws -> [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) throws -> [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) throws -> [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 751cc77d..cd215f33 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift @@ -16,33 +16,48 @@ 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)? - var currentSession: SessionRecord? + weak var storage: EmbraceStorage? + 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() } - let session = SessionRecord( - id: nextSessionId ?? .random, - state: state, - processId: ProcessIdentifier.current, - traceId: TestConstants.traceId, - spanId: TestConstants.spanId, - startTime: startTime - ) - didCallStartSession = true + + var session: EmbraceSession? + + if let storage = storage { + session = storage.addSession( + id: nextSessionId ?? .random, + processId: ProcessIdentifier.current, + state: state, + traceId: TestConstants.traceId, + spanId: TestConstants.spanId, + startTime: startTime + ) + } else { + session = MockSession( + id: nextSessionId ?? .random, + processId: ProcessIdentifier.current, + state: state, + traceId: TestConstants.traceId, + spanId: TestConstants.spanId, + startTime: startTime + ) + } + currentSession = session return session @@ -68,7 +83,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 011ce139..be5788e9 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyLogRepository.swift @@ -2,33 +2,49 @@ // Copyright © 2023 Embrace Mobile, Inc. All rights reserved. // +import Foundation import EmbraceStorageInternal import EmbraceCommonInternal +import OpenTelemetryApi +import TestSupport class SpyLogRepository: LogRepository { + var didCallFetchAll = false - var stubbedFetchAllResult: [LogRecord] = [] - func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) throws -> [LogRecord] { + var stubbedFetchAllResult: [EmbraceLog] = [] + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] { didCallFetchAll = true return stubbedFetchAllResult } var didCallRemoveLogs = false - func remove(logs: [LogRecord]) throws { + func remove(logs: [EmbraceLog]) { 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] + ) -> EmbraceLog? { didCallCreate = true - if let result = stubbedCreateCompletionResult { - completion(result) - } + + return MockLog( + 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..c37ca0a0 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpyStorage.swift @@ -5,6 +5,8 @@ import Foundation import EmbraceStorageInternal import EmbraceCommonInternal +import OpenTelemetryApi +import TestSupport class RandomError: Error, CustomNSError { static var errorDomain: String = "Embrace" @@ -13,118 +15,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] { + var stubbedFetchAllResources: [EmbraceMetadata] = [] + func fetchAllResources() -> [EmbraceMetadata] { didCallFetchAllResources = true - guard !shouldThrow else { - throw RandomError() - } return stubbedFetchAllResources } var didCallFetchResourcesForSessionId = false var fetchResourcesForSessionIdReceivedParameter: SessionIdentifier! - var stubbedFetchResourcesForSessionId: [MetadataRecord] = [] - func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) throws -> [MetadataRecord] { + var stubbedFetchResourcesForSessionId: [EmbraceMetadata] = [] + func fetchResourcesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { 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] { + var stubbedFetchResourcesForProcessId: [EmbraceMetadata] = [] + func fetchResourcesForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { 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] { + var stubbedFetchCustomPropertiesForSessionId: [EmbraceMetadata] = [] + func fetchCustomPropertiesForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { 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] { + var stubbedFetchPersonaTagsForSessionId: [EmbraceMetadata] = [] + func fetchPersonaTagsForSessionId(_ sessionId: SessionIdentifier) -> [EmbraceMetadata] { 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] { + var stubbedFetchPersonaTagsForProcessId: [EmbraceMetadata] = [] + func fetchPersonaTagsForProcessId(_ processId: ProcessIdentifier) -> [EmbraceMetadata] { 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] + ) -> EmbraceLog? { didCallCreate = true - if let result = stubbedCreateResult { - completion(result) - } + + return MockLog( + id: id, + processId: processId, + severity: severity, + body: body, + timestamp: timestamp, + attributes: attributes + ) } var didCallFetchAllExcludingProcessIdentifier = false - var stubbedFetchAllExcludingProcessIdentifier: [LogRecord] = [] + var stubbedFetchAllExcludingProcessIdentifier: [EmbraceLog] = [] var fetchAllExcludingProcessIdentifierReceivedParameter: ProcessIdentifier! - func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) throws -> [LogRecord] { + func fetchAll(excludingProcessIdentifier processIdentifier: ProcessIdentifier) -> [EmbraceLog] { didCallFetchAllExcludingProcessIdentifier = true - guard !shouldThrow else { - throw RandomError() - } fetchAllExcludingProcessIdentifierReceivedParameter = processIdentifier return stubbedFetchAllExcludingProcessIdentifier } var didCallRemoveLogs = false - var removeLogsReceivedParameter: [LogRecord] = [] - func remove(logs: [LogRecord]) throws { + var removeLogsReceivedParameter: [EmbraceLog] = [] + func remove(logs: [EmbraceLog]) { 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 deleted file mode 100644 index 8c54b986..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 { - self.init( - key: key, - value: value, - type: .customProperty, - lifespan: .session, - lifespanId: sessionId.toString - ) - } - - static func userMetadata(key: String, value: String) -> MetadataRecord { - .init( - key: key, - value: .string(value), - type: .customProperty, - lifespan: .session, - lifespanId: .random() - ) - } - - static func createResourceRecord(key: String, value: String) -> MetadataRecord { - .init( - key: key, - value: .string(value), - type: .resource, - lifespan: .session, - lifespanId: .random() - ) - } - - static func createPersonaTagRecord(value: String) -> MetadataRecord { - .init( - key: value, - value: .string(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 a2e689d9..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 { - .init( - id: id, - state: state, - processId: .random, - 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..edac14aa 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) - givenDatabase(withLogs: logs) + let uuid2 = UUID() + let secondLogToDelete = createInfoLog(withId: uuid2) - try sut.remove(logs: [ + let uuid3 = UUID() + createInfoLog(withId: uuid3) + + 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) -> EmbraceLog { + 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+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..489126f3 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,317 +14,58 @@ 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", 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)) - } + 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", type: .performance, data: Data(), startTime: Date() - ) - let span2 = SpanRecord( + )! + let span2 = storage.upsertSpan( 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 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_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") - } - - 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..d0a7c5e8 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, @@ -42,11 +42,8 @@ final class EmbraceStorage_SpanForSessionRecordTests: XCTestCase { data: Data(), startTime: startTime, endTime: endTime, - processIdentifier: processIdentifier - ) - try storage.upsertSpan(span) - - return span + processId: processIdentifier + )! } func sessionRecord( @@ -58,17 +55,17 @@ 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, endTime: endTime, lastHeartbeatTime: lastHeartBeat ?? startTime, coldStart: coldStart - ) + )! } // MARK: Tests @@ -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..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 { @@ -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)) @@ -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 { @@ -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) @@ -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 { @@ -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..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,12 +73,12 @@ class EmbraceUploadCacheTests: XCTestCase { data: Data(), attemptCount: 0, date: Date() - ) + )! 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..584e65b9 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() } + static func createInMemoryDb() throws -> EmbraceStorage { + 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() - } -} diff --git a/Tests/TestSupport/Mocks/DummyLogControllable.swift b/Tests/TestSupport/Mocks/DummyLogControllable.swift index 128a093f..0a26e591 100644 --- a/Tests/TestSupport/Mocks/DummyLogControllable.swift +++ b/Tests/TestSupport/Mocks/DummyLogControllable.swift @@ -25,5 +25,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/TestSupport/Mocks/Model/MockSession.swift b/Tests/TestSupport/Mocks/Model/MockSession.swift new file mode 100644 index 00000000..00ce6c10 --- /dev/null +++ b/Tests/TestSupport/Mocks/Model/MockSession.swift @@ -0,0 +1,62 @@ +// +// Copyright © 2025 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +import EmbraceCommonInternal + +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, + 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.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 + } +} + +public extension MockSession { + static func with(id: SessionIdentifier, state: SessionState) -> MockSession { + MockSession( + id: id, + processId: .random, + state: state, + traceId: "", + spanId: "", + startTime: Date() + ) + } +} 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"