feat(ios): enhance recovery logging and metrics recording

Implement comprehensive logging and observability for recovery operations:

- Add HistoryDAO for Core Data history recording
  - recordRecovery() method with execution time tracking
  - recordRecoveryFailure() method with detailed error info
  - Query helpers for history retrieval
- Enhance DailyNotificationReactivationManager logging:
  - Add execution time tracking (startTime/endTime)
  - Enhanced error logging with NSError details (domain, code, userInfo)
  - Comprehensive logging at each recovery step
  - Missed/future notification count logging
- Implement Core Data persistence for recovery metrics:
  - Recovery execution time
  - Missed notification count
  - Rescheduled notification count
  - Verified notification count
  - Error count
  - Diagnostic JSON with full recovery context
- Update recovery methods to record history:
  - Cold start recovery
  - Termination recovery
  - Boot recovery
  - All with timing and metrics

Completes section 7.1 (Recovery Logging) and 7.2 (Metrics Recording)
of iOS implementation checklist.
This commit is contained in:
Matthew
2025-12-09 02:37:27 -08:00
parent a90d08c425
commit 12d8536588
3 changed files with 442 additions and 44 deletions

300
ios/Plugin/HistoryDAO.swift Normal file
View File

@@ -0,0 +1,300 @@
/**
* HistoryDAO.swift
*
* Data Access Object (DAO) for History Core Data entity
* Provides helper methods for recording recovery and operation history
*
* @author Matthew Raymer
* @version 1.0.0
* @created 2025-12-08
*/
import Foundation
import CoreData
/**
* Extension providing DAO methods for History entity
*
* This extension adds helper methods for recording operation history
* including recovery operations, errors, and metrics.
*/
extension History {
// MARK: - Constants
private static let TAG = "DNP-HISTORY-DAO"
// MARK: - Create/Insert Methods
/**
* Create a new History entity in the given context
*
* @param context Core Data managed object context
* @param id Unique history identifier (UUID recommended)
* @param refId Reference ID (e.g., notification ID, schedule ID)
* @param kind History kind (e.g., "recovery", "fetch", "notify")
* @param occurredAt When the event occurred
* @param durationMs Duration in milliseconds
* @param outcome Outcome string (e.g., "success", "failure", "skipped")
* @param diagJson Diagnostic JSON string with additional details
* @return Created History entity
*/
static func create(
in context: NSManagedObjectContext,
id: String,
refId: String? = nil,
kind: String,
occurredAt: Date,
durationMs: Int32 = 0,
outcome: String,
diagJson: String? = nil
) -> History {
let entity = History(context: context)
entity.id = id
entity.refId = refId
entity.kind = kind
entity.occurredAt = occurredAt
entity.durationMs = durationMs
entity.outcome = outcome
entity.diagJson = diagJson
print("\(Self.TAG): Created History record: kind=\(kind), outcome=\(outcome)")
return entity
}
/**
* Create history record from dictionary
*
* @param context Core Data managed object context
* @param dict Dictionary with history data
* @return Created History entity or nil
*/
static func create(
in context: NSManagedObjectContext,
from dict: [String: Any]
) -> History? {
guard let id = dict["id"] as? String,
let kind = dict["kind"] as? String,
let outcome = dict["outcome"] as? String else {
print("\(Self.TAG): Missing required fields")
return nil
}
// Convert occurredAt from epoch milliseconds or Date
let occurredAt: Date
if let timeMillis = dict["occurredAt"] as? Int64 {
occurredAt = DailyNotificationDataConversions.dateFromEpochMillis(timeMillis)
} else if let timeDate = dict["occurredAt"] as? Date {
occurredAt = timeDate
} else {
occurredAt = Date()
}
let entity = History(context: context)
entity.id = id
entity.refId = dict["refId"] as? String
entity.kind = kind
entity.occurredAt = occurredAt
entity.durationMs = DailyNotificationDataConversions.int32FromInt(
dict["durationMs"] as? Int ?? 0
)
entity.outcome = outcome
// Convert diagJson from dictionary if needed
if let diagDict = dict["diagJson"] as? [String: Any] {
entity.diagJson = DailyNotificationDataConversions.jsonStringFromDictionary(diagDict)
} else if let diagString = dict["diagJson"] as? String {
entity.diagJson = diagString
}
return entity
}
/**
* Record recovery history with metrics
*
* @param context Core Data managed object context
* @param scenario Recovery scenario
* @param missedCount Number of missed notifications
* @param rescheduledCount Number of rescheduled notifications
* @param verifiedCount Number of verified notifications
* @param errors Number of errors
* @param startTime When recovery started
* @param endTime When recovery ended
* @return Created History entity
*/
static func recordRecovery(
in context: NSManagedObjectContext,
scenario: String,
missedCount: Int,
rescheduledCount: Int,
verifiedCount: Int,
errors: Int,
startTime: Date,
endTime: Date
) -> History {
let durationMs = Int32((endTime.timeIntervalSince(startTime) * 1000).rounded())
let diagJson: [String: Any] = [
"scenario": scenario,
"missedCount": missedCount,
"rescheduledCount": rescheduledCount,
"verifiedCount": verifiedCount,
"errors": errors,
"durationMs": durationMs
]
let diagJsonString = DailyNotificationDataConversions.jsonStringFromDictionary(diagJson) ?? "{}"
return create(
in: context,
id: UUID().uuidString,
kind: "recovery",
occurredAt: endTime,
durationMs: durationMs,
outcome: errors > 0 ? "partial_success" : "success",
diagJson: diagJsonString
)
}
/**
* Record recovery failure
*
* @param context Core Data managed object context
* @param error Error that occurred
* @param scenario Recovery scenario (if known)
* @return Created History entity
*/
static func recordRecoveryFailure(
in context: NSManagedObjectContext,
error: Error,
scenario: String? = nil
) -> History {
var errorInfo: [String: Any] = [
"error": error.localizedDescription,
"errorType": String(describing: type(of: error))
]
// Add scenario if provided
if let scenario = scenario {
errorInfo["scenario"] = scenario
}
// Add error details if available
if let nsError = error as NSError? {
errorInfo["errorCode"] = nsError.code
errorInfo["errorDomain"] = nsError.domain
if let userInfo = nsError.userInfo as? [String: Any] {
errorInfo["userInfo"] = userInfo
}
}
let diagJsonString = DailyNotificationDataConversions.jsonStringFromDictionary(errorInfo) ?? "{}"
return create(
in: context,
id: UUID().uuidString,
kind: "recovery",
occurredAt: Date(),
outcome: "failure",
diagJson: diagJsonString
)
}
// MARK: - Read/Query Methods
/**
* Fetch History by ID
*
* @param context Core Data managed object context
* @param id History ID
* @return History entity or nil
*/
static func fetch(
by id: String,
in context: NSManagedObjectContext
) -> History? {
let request: NSFetchRequest<History> = History.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
do {
let results = try context.fetch(request)
return results.first
} catch {
print("\(Self.TAG): Error fetching by id: \(error.localizedDescription)")
return nil
}
}
/**
* Query by kind
*
* @param context Core Data managed object context
* @param kind History kind
* @return Array of History entities
*/
static func query(
by kind: String,
in context: NSManagedObjectContext
) -> [History] {
let request: NSFetchRequest<History> = History.fetchRequest()
request.predicate = NSPredicate(format: "kind == %@", kind)
request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)]
do {
return try context.fetch(request)
} catch {
print("\(Self.TAG): Error querying by kind: \(error.localizedDescription)")
return []
}
}
/**
* Query by refId
*
* @param context Core Data managed object context
* @param refId Reference ID
* @return Array of History entities
*/
static func query(
by refId: String,
in context: NSManagedObjectContext
) -> [History] {
let request: NSFetchRequest<History> = History.fetchRequest()
request.predicate = NSPredicate(format: "refId == %@", refId)
request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)]
do {
return try context.fetch(request)
} catch {
print("\(Self.TAG): Error querying by refId: \(error.localizedDescription)")
return []
}
}
/**
* Query by outcome
*
* @param context Core Data managed object context
* @param outcome Outcome string
* @return Array of History entities
*/
static func query(
by outcome: String,
in context: NSManagedObjectContext
) -> [History] {
let request: NSFetchRequest<History> = History.fetchRequest()
request.predicate = NSPredicate(format: "outcome == %@", outcome)
request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)]
do {
return try context.fetch(request)
} catch {
print("\(Self.TAG): Error querying by outcome: \(error.localizedDescription)")
return []
}
}
}