learning_ai_clock/ios/ChronoMind/Shared/Diagnostics/CrashReporter.swift

153 lines
4.8 KiB
Swift

// Crash Reporter
// MetricKit-based crash and performance reporting for TestFlight/Production
import Foundation
import MetricKit
@MainActor
final class CrashReporter: NSObject, ObservableObject, MXMetricManagerSubscriber {
static let shared = CrashReporter()
@Published var lastCrashReport: Date?
@Published var diagnosticCount: Int = 0
private let persistenceKey = "chronomind-crash-reports"
override private init() {
super.init()
MXMetricManager.shared.add(self)
loadStats()
}
deinit {
MXMetricManager.shared.remove(self)
}
// MARK: - MXMetricManagerSubscriber
nonisolated func didReceive(_ payloads: [MXMetricPayload]) {
// MetricKit delivers daily aggregated metrics
Task { @MainActor in
for payload in payloads {
processMetricPayload(payload)
}
}
}
nonisolated func didReceive(_ payloads: [MXDiagnosticPayload]) {
// Crash diagnostics delivered after app restart following crash
Task { @MainActor in
for payload in payloads {
processDiagnosticPayload(payload)
}
self.diagnosticCount += payloads.count
self.lastCrashReport = Date()
self.saveStats()
}
}
// MARK: - Processing
private func processMetricPayload(_ payload: MXMetricPayload) {
// Log key performance metrics
if let launchTime = payload.applicationLaunchMetrics {
let resumeTime = launchTime.histogrammedResumeTime
logMetric("app_resume_time", histogram: resumeTime)
}
if let responsiveness = payload.applicationResponsivenessMetrics {
let hangTime = responsiveness.histogrammedApplicationHangTime
logMetric("hang_time", histogram: hangTime)
}
}
private func processDiagnosticPayload(_ payload: MXDiagnosticPayload) {
// Store crash data locally for feedback form
if let crashDiagnostics = payload.crashDiagnostics {
for crash in crashDiagnostics {
let report = CrashReport(
date: Date(),
exceptionType: crash.exceptionType?.description,
signal: crash.signal?.description,
terminationReason: crash.terminationReason?.description,
callStackTree: crash.callStackTree.jsonRepresentation()
)
storeCrashReport(report)
}
}
if let hangDiagnostics = payload.hangDiagnostics {
for hang in hangDiagnostics {
let report = CrashReport(
date: Date(),
exceptionType: nil,
signal: nil,
terminationReason: "Hang: \(hang.hangDuration.description)",
callStackTree: hang.callStackTree.jsonRepresentation()
)
storeCrashReport(report)
}
}
}
// MARK: - Storage
private func storeCrashReport(_ report: CrashReport) {
var reports = loadCrashReports()
reports.append(report)
// Keep last 50 reports
if reports.count > 50 {
reports = Array(reports.suffix(50))
}
if let data = try? JSONEncoder().encode(reports) {
UserDefaults.standard.set(data, forKey: persistenceKey)
}
}
func loadCrashReports() -> [CrashReport] {
guard let data = UserDefaults.standard.data(forKey: persistenceKey),
let reports = try? JSONDecoder().decode([CrashReport].self, from: data) else {
return []
}
return reports
}
func clearReports() {
UserDefaults.standard.removeObject(forKey: persistenceKey)
diagnosticCount = 0
}
private func loadStats() {
diagnosticCount = loadCrashReports().count
}
private func saveStats() {
// Stats are derived from stored reports
}
private func logMetric(_ name: String, histogram: MXHistogram<UnitDuration>) {
// In production, send to analytics service
// For now, just track locally
}
}
// MARK: - Crash Report Model
struct CrashReport: Codable, Identifiable {
let id: String
let date: Date
let exceptionType: String?
let signal: String?
let terminationReason: String?
let callStackData: Data?
init(date: Date, exceptionType: String?, signal: String?, terminationReason: String?, callStackTree: Data) {
self.id = UUID().uuidString
self.date = date
self.exceptionType = exceptionType
self.signal = signal
self.terminationReason = terminationReason
self.callStackData = callStackTree
}
}