153 lines
4.8 KiB
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
|
|
}
|
|
}
|