277 lines
9.2 KiB
Swift
277 lines
9.2 KiB
Swift
import Foundation
|
||
import UIKit
|
||
|
||
// MARK: - V1.0 轻量埋点管理器
|
||
|
||
final class AnalyticsManager {
|
||
static let shared = AnalyticsManager()
|
||
|
||
// MARK: - 配置
|
||
private let flushInterval: TimeInterval = 30 // 30 秒自动 flush
|
||
private let flushThreshold: Int = 20 // 队列满 20 条自动 flush
|
||
private let maxRetryEvents: Int = 500 // 离线缓存最多保留 500 条
|
||
|
||
// MARK: - 内部状态
|
||
private var eventQueue: [[String: Any]] = [] // 内存事件队列
|
||
private let queue = DispatchQueue(label: "com.wildgrowth.analytics", qos: .utility)
|
||
private var flushTimer: Timer?
|
||
private var sessionId: String = UUID().uuidString
|
||
private var sessionStartTime: Date = Date()
|
||
|
||
// MARK: - 设备上下文(启动时采集一次)
|
||
private lazy var deviceId: String = {
|
||
// 优先使用持久化的 deviceId,否则生成新的
|
||
if let stored = UserDefaults.standard.string(forKey: "analytics_device_id") {
|
||
return stored
|
||
}
|
||
let id = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||
UserDefaults.standard.set(id, forKey: "analytics_device_id")
|
||
return id
|
||
}()
|
||
|
||
private lazy var appVersion: String = {
|
||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
|
||
}()
|
||
|
||
private lazy var osVersion: String = {
|
||
UIDevice.current.systemVersion
|
||
}()
|
||
|
||
private lazy var deviceModel: String = {
|
||
var systemInfo = utsname()
|
||
uname(&systemInfo)
|
||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||
let identifier = machineMirror.children.reduce("") { id, element in
|
||
guard let value = element.value as? Int8, value != 0 else { return id }
|
||
return id + String(UnicodeScalar(UInt8(value)))
|
||
}
|
||
return identifier
|
||
}()
|
||
|
||
// MARK: - Init
|
||
|
||
private init() {
|
||
// 加载离线缓存的事件
|
||
loadCachedEvents()
|
||
// 启动定时 flush
|
||
startFlushTimer()
|
||
// 监听 App 生命周期
|
||
setupLifecycleObservers()
|
||
}
|
||
|
||
// MARK: - 公开 API
|
||
|
||
/// 记录一个事件
|
||
/// - Parameters:
|
||
/// - event: 事件名称(如 "app_launch", "discovery_view")
|
||
/// - properties: 自定义属性(可选)
|
||
func track(_ event: String, properties: [String: Any]? = nil) {
|
||
queue.async { [weak self] in
|
||
guard let self = self else { return }
|
||
|
||
let eventData: [String: Any] = [
|
||
"eventName": event,
|
||
"timestamp": ISO8601DateFormatter().string(from: Date()),
|
||
"sessionId": self.sessionId,
|
||
"properties": properties ?? [:]
|
||
]
|
||
|
||
self.eventQueue.append(eventData)
|
||
|
||
#if DEBUG
|
||
print("📊 [Analytics] track: \(event) | queue=\(self.eventQueue.count)")
|
||
#endif
|
||
|
||
// 达到阈值自动 flush
|
||
if self.eventQueue.count >= self.flushThreshold {
|
||
self.flushInternal()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 手动触发上报(进后台时调用)
|
||
func flush() {
|
||
queue.async { [weak self] in
|
||
self?.flushInternal()
|
||
}
|
||
}
|
||
|
||
/// 开始新会话(App 进入前台时调用)
|
||
func startNewSession() {
|
||
queue.async { [weak self] in
|
||
guard let self = self else { return }
|
||
self.sessionId = UUID().uuidString
|
||
self.sessionStartTime = Date()
|
||
#if DEBUG
|
||
print("📊 [Analytics] 新会话: \(self.sessionId)")
|
||
#endif
|
||
}
|
||
}
|
||
|
||
// MARK: - 内部逻辑
|
||
|
||
private func flushInternal() {
|
||
// 已在 self.queue 中执行,无需再 dispatch
|
||
guard !eventQueue.isEmpty else { return }
|
||
|
||
let eventsToSend = eventQueue
|
||
eventQueue.removeAll()
|
||
|
||
#if DEBUG
|
||
print("📊 [Analytics] flush: 发送 \(eventsToSend.count) 条事件")
|
||
#endif
|
||
|
||
// 构建请求体
|
||
let context: [String: Any] = [
|
||
"deviceId": deviceId,
|
||
"userId": UserManager.shared.currentUser?.id as Any,
|
||
"sessionId": sessionId,
|
||
"appVersion": appVersion,
|
||
"osVersion": osVersion,
|
||
"deviceModel": deviceModel,
|
||
"networkType": getNetworkType()
|
||
]
|
||
|
||
let body: [String: Any] = [
|
||
"events": eventsToSend,
|
||
"context": context
|
||
]
|
||
|
||
// 发送网络请求(不依赖 APIClient,独立轻量实现)
|
||
sendEvents(body: body, events: eventsToSend)
|
||
}
|
||
|
||
private func sendEvents(body: [String: Any], events: [[String: Any]]) {
|
||
let baseURL = APIClient.shared.baseURL
|
||
guard let url = URL(string: "\(baseURL)/api/analytics/events") else {
|
||
// URL 无效,缓存事件
|
||
cacheEvents(events)
|
||
return
|
||
}
|
||
|
||
var request = URLRequest(url: url)
|
||
request.httpMethod = "POST"
|
||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
request.timeoutInterval = 10 // 埋点请求 10 秒超时,不拖累体验
|
||
|
||
guard let httpBody = try? JSONSerialization.data(withJSONObject: body) else {
|
||
cacheEvents(events)
|
||
return
|
||
}
|
||
request.httpBody = httpBody
|
||
|
||
// 使用独立的 URLSession(不复用 APIClient 的 session)
|
||
URLSession.shared.dataTask(with: request) { [weak self] _, response, error in
|
||
if let error = error {
|
||
#if DEBUG
|
||
print("📊 [Analytics] ❌ 上报失败: \(error.localizedDescription)")
|
||
#endif
|
||
// 网络失败,缓存到本地
|
||
self?.cacheEvents(events)
|
||
return
|
||
}
|
||
|
||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
||
#if DEBUG
|
||
print("📊 [Analytics] ✅ 上报成功: \(events.count) 条")
|
||
#endif
|
||
} else {
|
||
self?.cacheEvents(events)
|
||
}
|
||
}.resume()
|
||
}
|
||
|
||
// MARK: - 离线缓存
|
||
|
||
private var cacheFileURL: URL {
|
||
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||
return dir.appendingPathComponent("analytics_cache.json")
|
||
}
|
||
|
||
private func cacheEvents(_ events: [[String: Any]]) {
|
||
queue.async { [weak self] in
|
||
guard let self = self else { return }
|
||
// 加载现有缓存
|
||
var cached = self.loadCachedEventsFromDisk()
|
||
cached.append(contentsOf: events)
|
||
// 限制缓存大小
|
||
if cached.count > self.maxRetryEvents {
|
||
cached = Array(cached.suffix(self.maxRetryEvents))
|
||
}
|
||
// 写入文件
|
||
if let data = try? JSONSerialization.data(withJSONObject: cached) {
|
||
try? data.write(to: self.cacheFileURL)
|
||
}
|
||
#if DEBUG
|
||
print("📊 [Analytics] 缓存 \(events.count) 条事件(总缓存: \(cached.count))")
|
||
#endif
|
||
}
|
||
}
|
||
|
||
private func loadCachedEvents() {
|
||
let cached = loadCachedEventsFromDisk()
|
||
if !cached.isEmpty {
|
||
eventQueue.append(contentsOf: cached)
|
||
// 清空缓存文件
|
||
try? FileManager.default.removeItem(at: cacheFileURL)
|
||
#if DEBUG
|
||
print("📊 [Analytics] 加载 \(cached.count) 条离线缓存事件")
|
||
#endif
|
||
}
|
||
}
|
||
|
||
private func loadCachedEventsFromDisk() -> [[String: Any]] {
|
||
guard let data = try? Data(contentsOf: cacheFileURL),
|
||
let array = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
||
return []
|
||
}
|
||
return array
|
||
}
|
||
|
||
// MARK: - 定时器
|
||
|
||
private func startFlushTimer() {
|
||
DispatchQueue.main.async { [weak self] in
|
||
guard let self = self else { return }
|
||
self.flushTimer = Timer.scheduledTimer(withTimeInterval: self.flushInterval, repeats: true) { [weak self] _ in
|
||
self?.flush()
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 生命周期监听
|
||
|
||
private func setupLifecycleObservers() {
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(appWillResignActive),
|
||
name: UIApplication.willResignActiveNotification,
|
||
object: nil
|
||
)
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(appDidBecomeActive),
|
||
name: UIApplication.didBecomeActiveNotification,
|
||
object: nil
|
||
)
|
||
}
|
||
|
||
@objc private func appWillResignActive() {
|
||
track("app_enter_background")
|
||
flush()
|
||
}
|
||
|
||
@objc private func appDidBecomeActive() {
|
||
startNewSession()
|
||
track("app_enter_foreground")
|
||
}
|
||
|
||
// MARK: - 网络类型检测(轻量实现)
|
||
|
||
private func getNetworkType() -> String {
|
||
// 简化实现:不引入 NWPathMonitor 依赖,直接返回 unknown
|
||
// 后续如需细分 WiFi/Cellular 可扩展
|
||
return "unknown"
|
||
}
|
||
}
|