import Foundation import UIKit import XuqmCoreSDK /// BugCollect 崩溃收集 SDK @MainActor public final class BugCollectSDK { public static let shared = BugCollectSDK() private var logLevel: LogLevel = .warn private var environment: String = "production" private var queue: LogQueue? private var crashCapture: CrashCapture? private var breadcrumbBuffer: [Breadcrumb] = [] private let breadcrumbLock = NSLock() private let maxBreadcrumbs = 50 private var pendingErrors: [IssueEvent] = [] private let pendingErrorsLock = NSLock() private let sdkName = "bugcollect.ios" private let sdkVersion = "1.0.0" private init() {} // MARK: - Public API /// 设置日志级别 public func setLogLevel(_ level: LogLevel) { self.logLevel = level } /// 设置环境 public func setEnvironment(_ env: String) { self.environment = env } /// 添加面包屑 public func addBreadcrumb( category: String, message: String, level: String = "info", data: [String: Any?] = [:] ) { breadcrumbLock.lock() defer { breadcrumbLock.unlock() } if breadcrumbBuffer.count >= maxBreadcrumbs { breadcrumbBuffer.removeFirst() } breadcrumbBuffer.append(Breadcrumb( timestamp: Int(Date().timeIntervalSince1970 * 1000), category: category, message: message, level: level, data: data )) } /// 记录事件 public func event(name: String, properties: [String: Any?] = [:]) { guard isReady() else { return } let logEvent = LogEvent( name: name, properties: properties, appKey: XuqmSDK.shared.requireConfig().appKey, userId: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId, platform: "ios", release: appVersion(), environment: environment, sdk: IssueEvent.SdkInfo(name: sdkName, version: sdkVersion) ) queue?.push(logEvent) FunnelTracker.shared.track(name: name, properties: properties) } /// 捕获错误 public func captureError(_ error: Error, tags: [String: Any?] = [:], fingerprint: String? = nil) { let crumbs = breadcrumbLock.lockAndRun { breadcrumbBuffer } let fp = fingerprint ?? Fingerprint.compute( type: String(describing: type(of: error)), message: error.localizedDescription, stacktrace: Thread.callStackSymbols.joined(separator: "\n") ) let issueEvent = IssueEvent( level: "error", platform: "ios", fingerprint: fp, appKey: XuqmSDK.shared.requireConfig().appKey, exception: IssueEvent.ExceptionInfo( type: String(describing: type(of: error)), value: error.localizedDescription, stacktrace: Thread.callStackSymbols.joined(separator: "\n") ), breadcrumbs: crumbs, release: appVersion(), environment: environment, userId: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId, user: IssueEvent.UserInfo(id: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId), device: buildDeviceInfo(), tags: tags ) if !isReady() { pendingErrorsLock.lock() pendingErrors.append(issueEvent) pendingErrorsLock.unlock() return } flushPendingErrors() queue?.push(issueEvent) } /// 捕获致命错误 public func captureCrash(_ error: Error, tags: [String: Any?] = [:]) { guard isReady() else { return } let crumbs = breadcrumbLock.lockAndRun { breadcrumbBuffer } let issueEvent = IssueEvent( level: "fatal", platform: "ios", fingerprint: Fingerprint.compute( type: String(describing: type(of: error)), message: error.localizedDescription, stacktrace: Thread.callStackSymbols.joined(separator: "\n") ), appKey: XuqmSDK.shared.requireConfig().appKey, exception: IssueEvent.ExceptionInfo( type: String(describing: type(of: error)), value: error.localizedDescription, stacktrace: Thread.callStackSymbols.joined(separator: "\n") ), breadcrumbs: crumbs, release: appVersion(), environment: environment, userId: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId, user: IssueEvent.UserInfo(id: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId), device: buildDeviceInfo(), tags: tags ) queue?.push(issueEvent) } /// 记录警告 public func warn(_ message: String, tags: [String: Any?] = [:]) { guard logLevel.rawValue <= LogLevel.warn.rawValue else { return } guard isReady() else { return } let crumbs = breadcrumbLock.lockAndRun { breadcrumbBuffer } let issueEvent = IssueEvent( level: "warning", platform: "ios", fingerprint: Fingerprint.compute(type: "Warning", message: message, stacktrace: ""), appKey: XuqmSDK.shared.requireConfig().appKey, exception: IssueEvent.ExceptionInfo(type: "Warning", value: message), breadcrumbs: crumbs, release: appVersion(), environment: environment, userId: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId, user: IssueEvent.UserInfo(id: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId), device: buildDeviceInfo(), tags: tags ) queue?.push(issueEvent) } /// 记录信息 public func info(_ message: String, tags: [String: Any?] = [:]) { guard logLevel.rawValue <= LogLevel.info.rawValue else { return } event(name: "__log_info", properties: tags.merging(["message": message]) { _, new in new }) } // MARK: - Lifecycle /// 启动崩溃捕获 public func startCrashCapture() { guard crashCapture == nil else { return } crashCapture = CrashCapture { [weak self] exception, stacktrace in self?.handleCrash(exception: exception, stacktrace: stacktrace) } crashCapture?.install() } /// 初始化队列 public func startQueue() { guard queue == nil else { return } let config = XuqmSDK.shared.requireConfig() queue = LogQueue(appKey: config.appKey, sdkName: sdkName, sdkVersion: sdkVersion) queue?.start() } // MARK: - Private private var isReady: Bool { XuqmSDK.shared.isInitialized && queue != nil } private func flushPendingErrors() { pendingErrorsLock.lock() let errors = pendingErrors pendingErrors = [] pendingErrorsLock.unlock() for error in errors { queue?.push(error) } } private func handleCrash(exception: NSException, stacktrace: String) { let crumbs = breadcrumbLock.lockAndRun { breadcrumbBuffer } let issueEvent = IssueEvent( level: "fatal", platform: "ios", fingerprint: Fingerprint.compute( type: exception.name.rawValue, message: exception.reason ?? "", stacktrace: stacktrace ), appKey: XuqmSDK.shared.requireConfig().appKey, exception: IssueEvent.ExceptionInfo( type: exception.name.rawValue, value: exception.reason ?? "", stacktrace: stacktrace ), breadcrumbs: crumbs, release: appVersion(), environment: environment, userId: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId, user: IssueEvent.UserInfo(id: XuqmSDK.shared.userId ?? XuqmSDK.shared.deviceId), device: buildDeviceInfo(), tags: [:] ) // 崩溃时同步写入磁盘 LogStorage.saveCrash(issueEvent) } private func appVersion() -> String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" } private func buildDeviceInfo() -> IssueEvent.DeviceInfo { let device = UIDevice.current let processInfo = ProcessInfo.processInfo // 使用 os_proc_available_memory() 获取可用内存(如果可用) let freeMemoryMb: Int if #available(iOS 13.0, *) { let availableMemory = os_proc_available_memory() freeMemoryMb = Int(availableMemory / 1024 / 1024) } else { // Fallback: 使用物理内存(不准确但有值) freeMemoryMb = Int(processInfo.physicalMemory / 1024 / 1024) } return IssueEvent.DeviceInfo( model: device.model, manufacturer: "Apple", osName: device.systemName, osVersion: device.systemVersion, locale: Locale.current.identifier, timezone: TimeZone.current.identifier, isEmulator: isSimulator(), freeMemoryMb: freeMemoryMb, buildType: isDebug() ? "debug" : "release" ) } private func isSimulator() -> Bool { #if targetEnvironment(simulator) return true #else return false #endif } private func isDebug() -> Bool { #if DEBUG return true #else return false #endif } } // MARK: - NSLock Extension private extension NSLock { func lockAndRun(_ block: () -> T) -> T { lock() defer { unlock() } return block() } }