修复: - warn() 添加 breadcrumbs 和 device 信息 - freeMemoryMb 改为使用 os_proc_available_memory() 获取可用内存 - 添加 iOS 13+ 版本检查 Co-Authored-By: Claude <noreply@anthropic.com>
303 行
9.6 KiB
Swift
303 行
9.6 KiB
Swift
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<T>(_ block: () -> T) -> T {
|
||
lock()
|
||
defer { unlock() }
|
||
return block()
|
||
}
|
||
}
|