XuqmGroup-iOSSDK/Sources/XuqmBugCollectSDK/internal/LogUploader.swift
XuqmGroup 227bc66bfd feat: 添加 iOS BugCollect 崩溃收集模块
新增模块: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>
2026-06-19 03:38:22 +08:00

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()
}
}