新增模块: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>
127 行
4.4 KiB
Swift
127 行
4.4 KiB
Swift
import Foundation
|
|
import XuqmCoreSDK
|
|
|
|
/// 日志上传器
|
|
final class LogUploader {
|
|
|
|
private let appKey: String
|
|
private let encoder: JSONEncoder = {
|
|
let e = JSONEncoder()
|
|
e.dateEncodingStrategy = .iso8601
|
|
return e
|
|
}()
|
|
|
|
init(appKey: String) {
|
|
self.appKey = appKey
|
|
}
|
|
|
|
/// 上传 Issue 事件
|
|
func uploadIssues(_ issues: [IssueEvent], completion: ((Bool) -> Void)? = nil) {
|
|
let baseURL = SDKEndpoints.bugCollectApiURL ?? SDKEndpoints.apiBaseURL
|
|
guard let url = URL(string: "\(baseURL)/bugcollect/v1/issues/batch") else {
|
|
completion?(false)
|
|
return
|
|
}
|
|
|
|
let body: [String: Any] = [
|
|
"sentAt": ISO8601DateFormatter().string(from: Date()),
|
|
"sdk": ["name": "bugcollect.ios", "version": "1.0.0"],
|
|
"events": issues.map { issue in
|
|
var event: [String: Any] = [
|
|
"type": "issue",
|
|
"level": issue.level,
|
|
"platform": issue.platform,
|
|
"fingerprint": issue.fingerprint,
|
|
"appKey": issue.appKey,
|
|
"release": issue.release,
|
|
"environment": issue.environment
|
|
]
|
|
if let exception = issue.exception {
|
|
event["exception"] = [
|
|
"type": exception.type,
|
|
"value": exception.value,
|
|
"stacktrace": exception.stacktrace ?? ""
|
|
]
|
|
}
|
|
if let userId = issue.userId {
|
|
event["userId"] = userId
|
|
}
|
|
if let user = issue.user {
|
|
event["user"] = ["id": user.id]
|
|
}
|
|
if let device = issue.device {
|
|
event["device"] = [
|
|
"model": device.model,
|
|
"manufacturer": device.manufacturer,
|
|
"osName": device.osName,
|
|
"osVersion": device.osVersion,
|
|
"locale": device.locale,
|
|
"timezone": device.timezone,
|
|
"isEmulator": device.isEmulator,
|
|
"freeMemoryMb": device.freeMemoryMb,
|
|
"buildType": device.buildType
|
|
]
|
|
}
|
|
return event
|
|
}
|
|
]
|
|
|
|
upload(url: url, body: body, completion: completion)
|
|
}
|
|
|
|
/// 上传日志事件
|
|
func uploadEvents(_ events: [LogEvent], completion: ((Bool) -> Void)? = nil) {
|
|
let baseURL = SDKEndpoints.bugCollectApiURL ?? SDKEndpoints.apiBaseURL
|
|
guard let url = URL(string: "\(baseURL)/bugcollect/v1/events/batch") else {
|
|
completion?(false)
|
|
return
|
|
}
|
|
|
|
let body: [String: Any] = [
|
|
"sentAt": ISO8601DateFormatter().string(from: Date()),
|
|
"sdk": ["name": "bugcollect.ios", "version": "1.0.0"],
|
|
"events": events.map { event in
|
|
var dict: [String: Any] = [
|
|
"type": "event",
|
|
"name": event.name,
|
|
"appKey": event.appKey,
|
|
"userId": event.userId,
|
|
"platform": event.platform,
|
|
"release": event.release,
|
|
"environment": event.environment
|
|
]
|
|
if let props = event.properties {
|
|
dict["properties"] = props.mapValues { $0.value }
|
|
}
|
|
return dict
|
|
}
|
|
]
|
|
|
|
upload(url: url, body: body, completion: completion)
|
|
}
|
|
|
|
private func upload(url: URL, body: [String: Any], completion: ((Bool) -> Void)?) {
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
// 添加认证 token
|
|
if let token = XuqmSDK.shared.token {
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
}
|
|
|
|
do {
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
} catch {
|
|
completion?(false)
|
|
return
|
|
}
|
|
|
|
URLSession.shared.dataTask(with: request) { _, response, error in
|
|
let success = error == nil &&
|
|
(response as? HTTPURLResponse)?.statusCode ?? 0 < 400
|
|
completion?(success)
|
|
}.resume()
|
|
}
|
|
}
|