XuqmCoreSDK - 新增 userId / token(nonisolated) / deviceId 公开属性,对齐 BugCollectSDK / WebViewSDK 引用 XuqmWebViewSDK - XWebViewTypes:新增 jsBridgeName / onClose / onPageError / onProgressChanged / onNavigationChanged - XWebViewView:注册双 bridge(ReactNativeWebView 内部 + 自定义 jsBridgeName)、KVO progress、nav delegate - XWebViewStandardHandlers:@MainActor,injectedJavaScript 改 nonisolated,兼容 macOS - 新增 import XuqmCoreSDK XuqmBugCollectSDK - 全局 #if canImport(UIKit) 兼容 macOS 编译 - isReady 从 computed var 改为 func 消除歧义 - LogQueue 修正 LogStorage 为 enum 静态方法调用 XuqmUpdateSDK - getDeviceInfo() #if canImport(UIKit) 兼容 macOS XuqmLicenseSDK - ensureInitializedFromSDK() 用 DispatchQueue.main.sync 访问 @MainActor 属性 CI - 重写 Jenkinsfile:增加 SNAPSHOT/Release 模式,统一版本号管理,修正 git URL,对齐 Android 流水线 - 新增 version.properties:统一追踪 SDK_VERSION - 更新 .gitignore:移除 JS/Java 噪音,补 Swift/Xcode 规则 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
310 行
9.8 KiB
Swift
310 行
9.8 KiB
Swift
import Foundation
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#endif
|
|
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 func isReady() -> Bool {
|
|
XuqmSDK.shared.initialized && 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 processInfo = ProcessInfo.processInfo
|
|
let freeMemoryMb = Int(processInfo.physicalMemory / 1024 / 1024)
|
|
|
|
#if canImport(UIKit)
|
|
let device = UIDevice.current
|
|
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"
|
|
)
|
|
#else
|
|
return IssueEvent.DeviceInfo(
|
|
model: "Mac",
|
|
manufacturer: "Apple",
|
|
osName: "macOS",
|
|
osVersion: ProcessInfo.processInfo.operatingSystemVersionString,
|
|
locale: Locale.current.identifier,
|
|
timezone: TimeZone.current.identifier,
|
|
isEmulator: false,
|
|
freeMemoryMb: freeMemoryMb,
|
|
buildType: isDebug() ? "debug" : "release"
|
|
)
|
|
#endif
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|