新增模块:XuqmBugCollectSDK - BugCollectSDK: 主入口,错误捕获 API - CrashCapture: 崩溃捕获(NSException + POSIX 信号) - IssueEvent/LogEvent: 数据模型 - Fingerprint: 错误指纹计算(SHA-256) - LogQueue: 内存队列 + 定时上传 - LogStorage: 崩溃文件持久化 - LogUploader: HTTP 上传到服务端 - FunnelTracker: 漏斗追踪 功能: - captureError/captureCrash 捕获错误 - addBreadcrumb 添加面包屑 - event 记录自定义事件 - warn/info 日志级别记录 - 自动初始化(onInitialize hook) Co-Authored-By: Claude <noreply@anthropic.com>
229 行
6.4 KiB
Swift
229 行
6.4 KiB
Swift
import Foundation
|
|
|
|
/// Issue 事件数据模型
|
|
public struct IssueEvent: Codable, Sendable {
|
|
public let level: String
|
|
public let platform: String
|
|
public let fingerprint: String
|
|
public let appKey: String
|
|
public let exception: ExceptionInfo?
|
|
public let breadcrumbs: [Breadcrumb]
|
|
public let release: String
|
|
public let environment: String
|
|
public let userId: String?
|
|
public let user: UserInfo?
|
|
public let device: DeviceInfo?
|
|
public let tags: [String: AnyCodable]?
|
|
public let sdk: SdkInfo?
|
|
|
|
public init(
|
|
level: String,
|
|
platform: String,
|
|
fingerprint: String,
|
|
appKey: String,
|
|
exception: ExceptionInfo? = nil,
|
|
breadcrumbs: [Breadcrumb] = [],
|
|
release: String,
|
|
environment: String,
|
|
userId: String? = nil,
|
|
user: UserInfo? = nil,
|
|
device: DeviceInfo? = nil,
|
|
tags: [String: Any?]? = nil,
|
|
sdk: SdkInfo? = nil
|
|
) {
|
|
self.level = level
|
|
self.platform = platform
|
|
self.fingerprint = fingerprint
|
|
self.appKey = appKey
|
|
self.exception = exception
|
|
self.breadcrumbs = breadcrumbs
|
|
self.release = release
|
|
self.environment = environment
|
|
self.userId = userId
|
|
self.user = user
|
|
self.device = device
|
|
self.tags = tags?.mapValues { AnyCodable($0) }
|
|
self.sdk = sdk
|
|
}
|
|
|
|
public struct ExceptionInfo: Codable, Sendable {
|
|
public let type: String
|
|
public let value: String
|
|
public let stacktrace: String?
|
|
|
|
public init(type: String, value: String, stacktrace: String? = nil) {
|
|
self.type = type
|
|
self.value = value
|
|
self.stacktrace = stacktrace
|
|
}
|
|
}
|
|
|
|
public struct UserInfo: Codable, Sendable {
|
|
public let id: String
|
|
|
|
public init(id: String) {
|
|
self.id = id
|
|
}
|
|
}
|
|
|
|
public struct DeviceInfo: Codable, Sendable {
|
|
public let model: String
|
|
public let manufacturer: String
|
|
public let osName: String
|
|
public let osVersion: String
|
|
public let locale: String
|
|
public let timezone: String
|
|
public let isEmulator: Bool
|
|
public let freeMemoryMb: Int
|
|
public let buildType: String
|
|
|
|
public init(
|
|
model: String,
|
|
manufacturer: String,
|
|
osName: String,
|
|
osVersion: String,
|
|
locale: String,
|
|
timezone: String,
|
|
isEmulator: Bool,
|
|
freeMemoryMb: Int,
|
|
buildType: String
|
|
) {
|
|
self.model = model
|
|
self.manufacturer = manufacturer
|
|
self.osName = osName
|
|
self.osVersion = osVersion
|
|
self.locale = locale
|
|
self.timezone = timezone
|
|
self.isEmulator = isEmulator
|
|
self.freeMemoryMb = freeMemoryMb
|
|
self.buildType = buildType
|
|
}
|
|
}
|
|
|
|
public struct SdkInfo: Codable, Sendable {
|
|
public let name: String
|
|
public let version: String
|
|
|
|
public init(name: String, version: String) {
|
|
self.name = name
|
|
self.version = version
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 面包屑
|
|
public struct Breadcrumb: Codable, Sendable {
|
|
public let timestamp: Int
|
|
public let category: String
|
|
public let message: String
|
|
public let level: String
|
|
public let data: [String: AnyCodable]?
|
|
|
|
public init(
|
|
timestamp: Int,
|
|
category: String,
|
|
message: String,
|
|
level: String,
|
|
data: [String: Any?]? = nil
|
|
) {
|
|
self.timestamp = timestamp
|
|
self.category = category
|
|
self.message = message
|
|
self.level = level
|
|
self.data = data?.mapValues { AnyCodable($0) }
|
|
}
|
|
}
|
|
|
|
/// 日志事件
|
|
public struct LogEvent: Codable, Sendable {
|
|
public let name: String
|
|
public let properties: [String: AnyCodable]?
|
|
public let appKey: String
|
|
public let userId: String
|
|
public let platform: String
|
|
public let release: String
|
|
public let environment: String
|
|
public let sdk: IssueEvent.SdkInfo
|
|
|
|
public init(
|
|
name: String,
|
|
properties: [String: Any?]? = nil,
|
|
appKey: String,
|
|
userId: String,
|
|
platform: String,
|
|
release: String,
|
|
environment: String,
|
|
sdk: IssueEvent.SdkInfo
|
|
) {
|
|
self.name = name
|
|
self.properties = properties?.mapValues { AnyCodable($0) }
|
|
self.appKey = appKey
|
|
self.userId = userId
|
|
self.platform = platform
|
|
self.release = release
|
|
self.environment = environment
|
|
self.sdk = sdk
|
|
}
|
|
}
|
|
|
|
/// 日志级别
|
|
public enum LogLevel: Int, Sendable {
|
|
case debug = 0
|
|
case info = 1
|
|
case warn = 2
|
|
case error = 3
|
|
case none = 4
|
|
}
|
|
|
|
/// AnyCodable 包装器
|
|
public struct AnyCodable: Codable, Sendable {
|
|
public let value: Any
|
|
|
|
public init(_ value: Any?) {
|
|
self.value = value ?? ()
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
if container.decodeNil() {
|
|
self.value = ()
|
|
} else if let bool = try? container.decode(Bool.self) {
|
|
self.value = bool
|
|
} else if let int = try? container.decode(Int.self) {
|
|
self.value = int
|
|
} else if let double = try? container.decode(Double.self) {
|
|
self.value = double
|
|
} else if let string = try? container.decode(String.self) {
|
|
self.value = string
|
|
} else if let array = try? container.decode([AnyCodable].self) {
|
|
self.value = array.map { $0.value }
|
|
} else if let dict = try? container.decode([String: AnyCodable].self) {
|
|
self.value = dict.mapValues { $0.value }
|
|
} else {
|
|
self.value = ()
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
switch value {
|
|
case is Void:
|
|
try container.encodeNil()
|
|
case let bool as Bool:
|
|
try container.encode(bool)
|
|
case let int as Int:
|
|
try container.encode(int)
|
|
case let double as Double:
|
|
try container.encode(double)
|
|
case let string as String:
|
|
try container.encode(string)
|
|
case let array as [Any]:
|
|
try container.encode(array.map { AnyCodable($0) })
|
|
case let dict as [String: Any]:
|
|
try container.encode(dict.mapValues { AnyCodable($0) })
|
|
default:
|
|
try container.encodeNil()
|
|
}
|
|
}
|
|
}
|