learning_ai_common_plat/packages/swift-platform-sdk/Sources/BLCrashReporter.swift
saravanakumardb1 23d14f33ea feat(swift-sdk): add 6 new components — BLBlobClient, BLKillSwitchClient, BLLicenseClient, BLBiometricAuth, BLCrashReporter, BLAuditLogger
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).
2026-02-28 22:38:43 -08:00

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
}
}