diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift index 34dafab63f..e1b8a8c650 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift @@ -83,6 +83,10 @@ extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { ipcScheduler.runQueuedOperations(showWebView: showWebView, completion: completion) } + + func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { + ipcScheduler.getDebugMetadata(completion: completion) + } } #endif diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 3f9361e6e5..432f131bad 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -114,4 +114,8 @@ extension IPCServiceManager: IPCServerInterface { browserWindowManager.show(domain: domain) } } + + func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { + scheduler.getDebugMetadata(completion: completion) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index 6ff680717b..ac8544c81e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -323,4 +323,10 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { } } } + + func getBackgroundAgentMetadata() async -> DBPUIDebugMetadata { + let metadata = await scanDelegate?.getBackgroundAgentMetadata() + + return mapper.mapToUIDebugMetadata(metadata: metadata, brokerProfileQueryData: brokerProfileQueryData) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index f651a62440..07f423b9fe 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -164,6 +164,15 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { // If you add a completion block, please remember to call it here too! }) } + + public func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { + xpc.execute(call: { server in + server.getDebugMetadata(completion: completion) + }, xpcReplyErrorHandler: { error in + os_log("Error \(error.localizedDescription)") + completion(nil) + }) + } } // MARK: - Incoming communication from the server diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift index 6f9bb8aa9f..cfb11f5187 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift @@ -23,7 +23,6 @@ import Common /// A scheduler that works through IPC to request the scheduling to a different process /// public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionScheduler { - private let ipcClient: DataBrokerProtectionIPCClient public init(ipcClient: DataBrokerProtectionIPCClient) { @@ -67,4 +66,8 @@ public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionSchedul public func runAllOperations(showWebView: Bool) { ipcClient.runAllOperations(showWebView: showWebView) } + + public func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { + ipcClient.getDebugMetadata(completion: completion) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index 33594703cb..9027e1d275 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -19,6 +19,60 @@ import Foundation import XPCHelper +@objc(DBPBackgroundAgentMetadata) +public final class DBPBackgroundAgentMetadata: NSObject, NSSecureCoding { + enum Consts { + static let backgroundAgentVersionKey = "backgroundAgentVersion" + static let isAgentRunningKey = "isAgentRunning" + static let agentSchedulerStateKey = "agentSchedulerState" + static let lastSchedulerSessionStartTimestampKey = "lastSchedulerSessionStartTimestamp" + } + + public static var supportsSecureCoding: Bool = true + + let backgroundAgentVersion: String + let isAgentRunning: Bool + let agentSchedulerState: String + let lastSchedulerSessionStartTimestamp: Double? + + init(backgroundAgentVersion: String, + isAgentRunning: Bool, + agentSchedulerState: String, + lastSchedulerSessionStartTimestamp: Double?) { + self.backgroundAgentVersion = backgroundAgentVersion + self.isAgentRunning = isAgentRunning + self.agentSchedulerState = agentSchedulerState + self.lastSchedulerSessionStartTimestamp = lastSchedulerSessionStartTimestamp + } + + public init?(coder: NSCoder) { + guard let backgroundAgentVersion = coder.decodeObject(of: NSString.self, + forKey: Consts.backgroundAgentVersionKey) as? String, + let agentSchedulerState = coder.decodeObject(of: NSString.self, + forKey: Consts.agentSchedulerStateKey) as? String else { + return nil + } + + self.backgroundAgentVersion = backgroundAgentVersion + self.isAgentRunning = coder.decodeBool(forKey: Consts.isAgentRunningKey) + self.agentSchedulerState = agentSchedulerState + self.lastSchedulerSessionStartTimestamp = coder.decodeObject( + of: NSNumber.self, + forKey: Consts.lastSchedulerSessionStartTimestampKey + )?.doubleValue + } + + public func encode(with coder: NSCoder) { + coder.encode(self.backgroundAgentVersion as NSString, forKey: Consts.backgroundAgentVersionKey) + coder.encode(self.isAgentRunning, forKey: Consts.isAgentRunningKey) + coder.encode(self.agentSchedulerState as NSString, forKey: Consts.agentSchedulerStateKey) + + if let lastSchedulerSessionStartTimestamp = self.lastSchedulerSessionStartTimestamp { + coder.encode(lastSchedulerSessionStartTimestamp as NSNumber, forKey: Consts.lastSchedulerSessionStartTimestampKey) + } + } +} + /// This protocol describes the server-side IPC interface for controlling the tunnel /// public protocol IPCServerInterface: AnyObject { @@ -51,6 +105,9 @@ public protocol IPCServerInterface: AnyObject { /// Opens a browser window with the specified domain /// func openBrowser(domain: String) + + /// Returns background agent metadata for debugging purposes + func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) } /// This protocol describes the server-side XPC interface. @@ -89,6 +146,8 @@ protocol XPCServerInterface { /// Opens a browser window with the specified domain /// func openBrowser(domain: String) + + func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) } public final class DataBrokerProtectionIPCServer { @@ -171,4 +230,8 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { func openBrowser(domain: String) { serverDelegate?.openBrowser(domain: domain) } + + func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { + serverDelegate?.getDebugMetadata(completion: completion) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index dc664cf412..d9dc3d80d0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -185,6 +185,50 @@ struct DBPUIScanHistory: DBPUISendableMessage { let sitesScanned: Int } +struct DBPUIDebugMetadata: DBPUISendableMessage { + let lastRunAppVersion: String + let lastRunAgentVersion: String? + let isAgentRunning: Bool + let lastSchedulerOperationType: String? // scan or optOut + let lastSchedulerOperationTimestamp: Double? + let lastSchedulerOperationBrokerUrl: String? + let lastSchedulerErrorMessage: String? + let lastSchedulerErrorTimestamp: Double? + let lastSchedulerSessionStartTimestamp: Double? + let agentSchedulerState: String? // stopped, running or idle + let lastStartedSchedulerOperationType: String? + let lastStartedSchedulerOperationTimestamp: Double? + let lastStartedSchedulerOperationBrokerUrl: String? + + init(lastRunAppVersion: String, + lastRunAgentVersion: String? = nil, + isAgentRunning: Bool = false, + lastSchedulerOperationType: String? = nil, + lastSchedulerOperationTimestamp: Double? = nil, + lastSchedulerOperationBrokerUrl: String? = nil, + lastSchedulerErrorMessage: String? = nil, + lastSchedulerErrorTimestamp: Double? = nil, + lastSchedulerSessionStartTimestamp: Double? = nil, + agentSchedulerState: String? = nil, + lastStartedSchedulerOperationType: String? = nil, + lastStartedSchedulerOperationTimestamp: Double? = nil, + lastStartedSchedulerOperationBrokerUrl: String? = nil) { + self.lastRunAppVersion = lastRunAppVersion + self.lastRunAgentVersion = lastRunAgentVersion + self.isAgentRunning = isAgentRunning + self.lastSchedulerOperationType = lastSchedulerOperationType + self.lastSchedulerOperationTimestamp = lastSchedulerOperationTimestamp + self.lastSchedulerOperationBrokerUrl = lastSchedulerOperationBrokerUrl + self.lastSchedulerErrorMessage = lastSchedulerErrorMessage + self.lastSchedulerErrorTimestamp = lastSchedulerErrorTimestamp + self.lastSchedulerSessionStartTimestamp = lastSchedulerSessionStartTimestamp + self.agentSchedulerState = agentSchedulerState + self.lastStartedSchedulerOperationType = lastStartedSchedulerOperationType + self.lastStartedSchedulerOperationTimestamp = lastStartedSchedulerOperationTimestamp + self.lastStartedSchedulerOperationBrokerUrl = lastStartedSchedulerOperationBrokerUrl + } +} + extension DBPUIInitialScanState { static var empty: DBPUIInitialScanState { .init(resultsFound: [DBPUIDataBrokerProfileMatch](), diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index 570a8f1e60..3ca4eeb399 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -25,6 +25,7 @@ import Common protocol DBPUIScanOps: AnyObject { func startScan() -> Bool func updateCacheWithCurrentScans() async + func getBackgroundAgentMetadata() async -> DBPBackgroundAgentMetadata? } final class DBPUIViewModel { @@ -86,4 +87,12 @@ extension DBPUIViewModel: DBPUIScanOps { pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DBPUIViewModel.updateCacheWithCurrentScans")) } } + + func getBackgroundAgentMetadata() async -> DBPBackgroundAgentMetadata? { + return await withCheckedContinuation { continuation in + scheduler.getDebugMetadata { metadata in + continuation.resume(returning: metadata) + } + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift index 5427cfec47..222377923b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift @@ -38,4 +38,5 @@ final class DataBrokerProtectionNoOpScheduler: DataBrokerProtectionScheduler { func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func scanAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func runAllOperations(showWebView: Bool) { } + func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 1c2cebe3eb..fdd8137ba9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -77,6 +77,10 @@ public protocol DataBrokerProtectionScheduler { func scanAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runAllOperations(showWebView: Bool) + + /// Debug operations + + func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) } extension DataBrokerProtectionScheduler { @@ -121,6 +125,8 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch public var statusPublisher: Published.Publisher { $status } + private var lastSchedulerSessionStartTimestamp: Date? + private lazy var dataBrokerProcessor: DataBrokerProtectionProcessor = { let runnerProvider = DataBrokerOperationRunnerProvider(privacyConfigManager: privacyConfigManager, @@ -174,6 +180,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch completion(.finished) return } + self.lastSchedulerSessionStartTimestamp = Date() self.status = .running os_log("Scheduler running...", log: .dataBrokerProtection) self.dataBrokerProcessor.runQueuedOperations(showWebView: showWebView) { [weak self] errors in @@ -289,4 +296,31 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch completion?(errors) }) } + + public func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { + if let backgroundAgentVersion = Bundle.main.releaseVersionNumber, let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String { + completion(DBPBackgroundAgentMetadata(backgroundAgentVersion: backgroundAgentVersion + " (build: \(buildNumber))", + isAgentRunning: status == .running, + agentSchedulerState: status.toString, + lastSchedulerSessionStartTimestamp: lastSchedulerSessionStartTimestamp?.timeIntervalSince1970)) + } else { + completion(DBPBackgroundAgentMetadata(backgroundAgentVersion: "ERROR: Error fetching background agent version", + isAgentRunning: status == .running, + agentSchedulerState: status.toString, + lastSchedulerSessionStartTimestamp: lastSchedulerSessionStartTimestamp?.timeIntervalSince1970)) + } + } +} + +extension DataBrokerProtectionSchedulerStatus { + var toString: String { + switch self { + case .idle: + return "idle" + case .running: + return "running" + case .stopped: + return "stopped" + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index f99df1e614..31532d38d2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -37,6 +37,7 @@ protocol DBPUICommunicationDelegate: AnyObject { func getInitialScanState() async -> DBPUIInitialScanState func getMaintananceScanState() async -> DBPUIScanAndOptOutMaintenanceState func getDataBrokers() async -> [DBPUIDataBroker] + func getBackgroundAgentMetadata() async -> DBPUIDebugMetadata } enum DBPUIReceivedMethodName: String { @@ -55,6 +56,7 @@ enum DBPUIReceivedMethodName: String { case initialScanStatus case maintenanceScanStatus case getDataBrokers + case getBackgroundAgentMetadata } enum DBPUISendableMethodName: String { @@ -71,7 +73,7 @@ struct DBPUICommunicationLayer: Subfeature { weak var delegate: DBPUICommunicationDelegate? private enum Constants { - static let version = 2 + static let version = 3 } internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable) { @@ -104,6 +106,7 @@ struct DBPUICommunicationLayer: Subfeature { case .initialScanStatus: return initialScanStatus case .maintenanceScanStatus: return maintenanceScanStatus case .getDataBrokers: return getDataBrokers + case .getBackgroundAgentMetadata: return getBackgroundAgentMetadata } } @@ -281,6 +284,10 @@ struct DBPUICommunicationLayer: Subfeature { return DBPUIDataBrokerList(dataBrokers: dataBrokers) } + func getBackgroundAgentMetadata(params: Any, origin: WKScriptMessage) async throws -> Encodable? { + return await delegate?.getBackgroundAgentMetadata() + } + func sendMessageToUI(method: DBPUISendableMethodName, params: DBPUISendableMessage, into webView: WKWebView) { broker?.push(method: method.rawValue, params: params, for: self, into: webView) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index d57e2d2442..87ccad2494 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -17,6 +17,7 @@ // import Foundation +import Common struct MapperToUI { @@ -115,7 +116,7 @@ struct MapperToUI { let mirrorSiteMatch = mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) if let extractedProfileRemovedDate = extractedProfile.removedDate, - mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { + mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { removedProfiles.append(mirrorSiteMatch) } else { inProgressOptOuts.append(mirrorSiteMatch) @@ -130,11 +131,11 @@ struct MapperToUI { value.compactMap { match in guard let removedDate = match.date else { return nil } return DBPUIOptOutMatch(dataBroker: key, - matches: value.count, - name: match.name, - alternativeNames: match.alternativeNames, - addresses: match.addresses, - date: removedDate) + matches: value.count, + name: match.name, + alternativeNames: match.alternativeNames, + addresses: match.addresses, + date: removedDate) } }.flatMap { $0 } @@ -214,6 +215,67 @@ struct MapperToUI { return DBPUIScanDate(date: scansHappeningInTheNextEightDays.first!.date!, dataBrokers: scansHappeningInTheNextEightDays) } } + + func mapToUIDebugMetadata(metadata: DBPBackgroundAgentMetadata?, brokerProfileQueryData: [BrokerProfileQueryData]) -> DBPUIDebugMetadata { + let currentAppVersion = Bundle.main.fullVersionNumber ?? "ERROR: Error fetching app version" + + guard let metadata = metadata else { + return DBPUIDebugMetadata(lastRunAppVersion: currentAppVersion, isAgentRunning: false) + } + + let lastOperation = brokerProfileQueryData.lastOperation + let lastStartedOperation = brokerProfileQueryData.lastStartedOperation + let lastError = brokerProfileQueryData.lastOperationThatErrored + + let lastOperationBrokerURL = brokerProfileQueryData.filter { $0.dataBroker.id == lastOperation?.brokerId }.first?.dataBroker.url + let lastStartedOperationBrokerURL = brokerProfileQueryData.filter { $0.dataBroker.id == lastStartedOperation?.brokerId }.first?.dataBroker.url + + let metadataUI = DBPUIDebugMetadata(lastRunAppVersion: currentAppVersion, + lastRunAgentVersion: metadata.backgroundAgentVersion, + isAgentRunning: true, + lastSchedulerOperationType: lastOperation?.toString, + lastSchedulerOperationTimestamp: lastOperation?.lastRunDate?.timeIntervalSince1970.withoutDecimals, + lastSchedulerOperationBrokerUrl: lastOperationBrokerURL, + lastSchedulerErrorMessage: lastError?.error, + lastSchedulerErrorTimestamp: lastError?.date.timeIntervalSince1970.withoutDecimals, + lastSchedulerSessionStartTimestamp: metadata.lastSchedulerSessionStartTimestamp, + agentSchedulerState: metadata.agentSchedulerState, + lastStartedSchedulerOperationType: lastStartedOperation?.toString, + lastStartedSchedulerOperationTimestamp: lastStartedOperation?.historyEvents.closestHistoryEvent?.date.timeIntervalSince1970.withoutDecimals, + lastStartedSchedulerOperationBrokerUrl: lastStartedOperationBrokerURL) + +#if DEBUG + do { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let jsonData = try encoder.encode(metadataUI) + if let jsonString = String(data: jsonData, encoding: .utf8) { + os_log("Metadata: %{public}s", log: OSLog.default, type: .info, jsonString) + } + } catch { + os_log("Error encoding struct to JSON: %{public}@", log: OSLog.default, type: .error, error.localizedDescription) + } +#endif + + return metadataUI + } +} + +extension Bundle { + var fullVersionNumber: String? { + guard let appVersion = self.releaseVersionNumber, + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String else { + return nil + } + + return appVersion + " (build: \(buildNumber))" + } +} + +extension TimeInterval { + var withoutDecimals: Double { + Double(Int(self)) + } } extension Date { @@ -241,6 +303,10 @@ extension String { fileprivate extension BrokerProfileQueryData { + var closestHistoryEvent: HistoryEvent? { + events.sorted(by: { $0.date > $1.date }).first + } + var sitesScanned: [String] { if scanOperationData.lastRunDate != nil { let scanEvents = scanOperationData.scanStartedEvents() @@ -289,6 +355,81 @@ fileprivate extension Array where Element == BrokerProfileQueryData { return 1 + broker.mirrorSites.filter { $0.shouldWeIncludeMirrorSite() }.count } } + + var lastOperation: BrokerOperationData? { + let allOperations = flatMap { $0.operationsData } + let lastOperation = allOperations.sorted(by: { + if let date1 = $0.lastRunDate, let date2 = $1.lastRunDate { + return date1 > date2 + } else if $0.lastRunDate != nil { + return true + } else { + return false + } + }).first + + return lastOperation + } + + var lastOperationThatErrored: HistoryEvent? { + let lastError = flatMap { $0.operationsData } + .flatMap { $0.historyEvents } + .filter { $0.isError } + .sorted(by: { $0.date > $1.date }) + .first + + return lastError + } + + var lastStartedOperation: BrokerOperationData? { + let allOperations = flatMap { $0.operationsData } + + return allOperations.sorted(by: { + if let date1 = $0.historyEvents.closestHistoryEvent?.date, let date2 = $1.historyEvents.closestHistoryEvent?.date { + return date1 > date2 + } else if $0.historyEvents.closestHistoryEvent?.date != nil { + return true + } else { + return false + } + }).first + } +} + +fileprivate extension BrokerOperationData { + var toString: String { + if (self as? OptOutOperationData) != nil { + return "optOut" + } else { + return "scan" + } + } +} + +fileprivate extension Array where Element == HistoryEvent { + var closestHistoryEvent: HistoryEvent? { + self.sorted(by: { $0.date > $1.date }).first + } +} + +extension HistoryEvent { + + var isError: Bool { + switch type { + case .error: + return true + default: + return false + } + } + + var error: String? { + switch type { + case .error(let error): + return error.name + default: return nil + } + } } fileprivate extension MirrorSite {