New SDK components extracted from product apps: - BLBlobClient — Azure Blob Storage upload via SAS tokens (from LysnrAI BlobService) - BLKillSwitchClient — Kill switch check from platform-service (from LysnrAI KillSwitchService) - BLLicenseClient — License key activation + status (from LysnrAI LicenseService) - BLBiometricAuth — Face ID / Touch ID wrapper (from LysnrAI BiometricAuth) - BLCrashReporter — MetricKit crash reporting (from ChronoMind CrashReporter) - BLAuditLogger — Local rotating JSON audit log (from LysnrAI AuditLogger) SDK now has 13 source files. Updated README with full component table and migration status (3 apps fully migrated, 18 wrappers total).
130 lines
4.4 KiB
Swift
130 lines
4.4 KiB
Swift
// ── Crash Reporter ──────────────────────────────────────────
|
|
// Generic MetricKit-based crash and performance reporting.
|
|
// Stores crash diagnostics locally for debugging and feedback forms.
|
|
// Product apps configure with a product-specific persistence key.
|
|
|
|
import Foundation
|
|
import MetricKit
|
|
|
|
/// Crash report model stored locally.
|
|
public struct BLCrashReport: Codable, Identifiable, Sendable {
|
|
public let id: String
|
|
public let date: Date
|
|
public let exceptionType: String?
|
|
public let signal: String?
|
|
public let terminationReason: String?
|
|
public let callStackData: Data?
|
|
|
|
public 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
|
|
}
|
|
}
|
|
|
|
/// Generic MetricKit crash reporter for all ByteLyst iOS apps.
|
|
/// Subscribes to MetricKit, stores crash reports in UserDefaults.
|
|
@MainActor
|
|
public final class BLCrashReporter: NSObject, ObservableObject, MXMetricManagerSubscriber {
|
|
|
|
private let persistenceKey: String
|
|
private let maxReports: Int
|
|
|
|
@Published public var lastCrashReport: Date?
|
|
@Published public var diagnosticCount: Int = 0
|
|
|
|
public init(productId: String, maxReports: Int = 50) {
|
|
self.persistenceKey = "\(productId)-crash-reports"
|
|
self.maxReports = maxReports
|
|
super.init()
|
|
MXMetricManager.shared.add(self)
|
|
loadStats()
|
|
}
|
|
|
|
deinit {
|
|
MXMetricManager.shared.remove(self)
|
|
}
|
|
|
|
// MARK: - MXMetricManagerSubscriber
|
|
|
|
nonisolated public func didReceive(_ payloads: [MXMetricPayload]) {
|
|
// MetricKit delivers daily aggregated metrics — no action needed by default
|
|
}
|
|
|
|
nonisolated public func didReceive(_ payloads: [MXDiagnosticPayload]) {
|
|
Task { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
for payload in payloads {
|
|
self.processDiagnosticPayload(payload)
|
|
}
|
|
self.diagnosticCount += payloads.count
|
|
self.lastCrashReport = Date()
|
|
}
|
|
}
|
|
|
|
// MARK: - Public API
|
|
|
|
/// Get all stored crash reports.
|
|
public func loadCrashReports() -> [BLCrashReport] {
|
|
guard let data = UserDefaults.standard.data(forKey: persistenceKey),
|
|
let reports = try? JSONDecoder().decode([BLCrashReport].self, from: data) else {
|
|
return []
|
|
}
|
|
return reports
|
|
}
|
|
|
|
/// Clear all stored crash reports.
|
|
public func clearReports() {
|
|
UserDefaults.standard.removeObject(forKey: persistenceKey)
|
|
diagnosticCount = 0
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func processDiagnosticPayload(_ payload: MXDiagnosticPayload) {
|
|
if let crashDiagnostics = payload.crashDiagnostics {
|
|
for crash in crashDiagnostics {
|
|
let report = BLCrashReport(
|
|
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 = BLCrashReport(
|
|
date: Date(),
|
|
exceptionType: nil,
|
|
signal: nil,
|
|
terminationReason: "Hang: \(hang.hangDuration.description)",
|
|
callStackTree: hang.callStackTree.jsonRepresentation()
|
|
)
|
|
storeCrashReport(report)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func storeCrashReport(_ report: BLCrashReport) {
|
|
var reports = loadCrashReports()
|
|
reports.append(report)
|
|
if reports.count > maxReports {
|
|
reports = Array(reports.suffix(maxReports))
|
|
}
|
|
if let data = try? JSONEncoder().encode(reports) {
|
|
UserDefaults.standard.set(data, forKey: persistenceKey)
|
|
}
|
|
}
|
|
|
|
private func loadStats() {
|
|
diagnosticCount = loadCrashReports().count
|
|
}
|
|
}
|