// // YXTracking.swift // YXTrackingSDK // // Created by 1234 on 2026/1/27. // import UIKit import Network /// 远轩埋点SDK实现类 四种上报机制 /// 1、初始化时触发:用户在初始化SDK时,会将缓存的事件数据上报。 /// 2、定时/定量触发:数据条数达到阈值(默认20条)或定时器到期(默认600秒)时会将缓存的事件数据上报。 /// 3、事件触发:用户在添加事件时可选择立即上报。 /// 4、应用进入后台和即将被系统终止时触发:用户在应用内停留一段时间后,手动切换到其他应用或桌面时,或即将被系统终止时,会将缓存的事件数据上报。 /// 5、网络状态变化时触发:当应用网络状态发生变化时,会将缓存的事件数据上报。 public class YXTracking: NSObject { static let shared = YXTracking() private var host: String = "" // 主机地址 private var cacheCount: Int = 20 // 缓存数量 private var pushInterval: Int = 600 // 上报周期(单位:秒) private var enableLog: Bool = false // 是否开启调试日志 private var eventRequestResult: EventRequestResult = EventRequestResult() // 事件请求参数 private var systemAllDimInfoResponseResult: SystemAllDimInfoResponseResult = SystemAllDimInfoResponseResult() // 系统所有维度信息 private var isPushingEventList: Bool = false // 是否正在推送事件列表 /// 初始化SDK /// - Parameters: /// - host: 主机地址 /// - systemCode: 系统编码 /// - cacheCount: 缓存数量(可选) /// - pushInterval: 上报周期(可选) /// - enableLog: 是否开启调试日志(可选) /// - Returns: 无返回值 public class func initWithSystemCode(host: String, systemCode: String, cacheCount: Int?, pushInterval: Int?, enableLog: Bool?) { // 校验主机地址是否为空 if host.isEmpty { YXTracking.shared.error(message: "SDK config failed, host is empty!"); return; } // 校验系统编码是否为空 if systemCode.isEmpty { YXTracking.shared.error(message: "SDK config failed, system code is empty!"); return; } // 设置主机地址和缓存数量、上报周期、是否开启调试日志 YXTracking.shared.host = host if cacheCount != nil { YXTracking.shared.cacheCount = cacheCount! } if pushInterval != nil { YXTracking.shared.cacheCount = pushInterval! } if enableLog != nil { YXTracking.shared.enableLog = enableLog! } // 打印日志信息 YXTracking.shared.log(message: "SDK config success!"); // 添加网络状态变化通知监听 网络状态发生变化时触发缓存事件推送 YXTracking.shared.addNetworkReachabilityObserver() // 添加应用进入后台和即将被系统终止的通知监听 应用进入后台时或即将被系统终止时触发缓存事件推送 YXTracking.shared.addEnterBackgroundNotificationObserver() YXTracking.shared.addWillTerminateNotificationObserver() // 重置事件请求参数中的系统编码和客户端类型 YXTracking.shared.eventRequestResult.system_code = systemCode YXTracking.shared.eventRequestResult.clientType = 2 // 获取设备信息 YXTracking.shared.getDeviceInfo() // 获取系统所有维度信息 YXTracking.shared.getSystemAllDimInfo() } /// 添加事件 /// - Parameters: /// - eventType: 事件类型 /// - page: 事件页面controller名称 /// - buttonId: 事件按钮名称 /// - url: 事件action /// - customTags: 自定义标签数组(可选) /// - pushNow: 是否立即推送 /// - Returns: 无返回值 public class func addEvent(eventType: String, page: String, buttonId: String, url: String, customTags: Dictionary?, pushNow: Bool) { // 重置事件请求参数中的事件类型 YXTracking.shared.eventRequestResult.eventType = eventType // 重置事件请求参数中的事件参数 let eventParams = EventParams(page: page, buttonId: buttonId, url: url) YXTracking.shared.eventRequestResult.eventParams = eventParams // 重置事件请求参数中的自定义标签 YXTracking.shared.eventRequestResult.customTags = customTags // 重置事件请求参数中的时间戳 YXTracking.shared.getTimestamp() YXTracking.shared.log(message: "SDK add event:\n\(YXTracking.shared.eventRequestResult)"); // 校验事件类型是否在系统维度信息中存在 if let systemEventTypes = YXTracking.shared.systemAllDimInfoResponseResult.systemEventTypes, systemEventTypes.count > 0 { guard !eventType.isEmpty else { YXTracking.shared.error(message: "SDK push event error, event type is empty!"); return } if !systemEventTypes.contains(where: { $0.event_code == eventType }) { YXTracking.shared.error(message: "SDK push event error, event type not in systemEventTypes!"); return; } } else { YXTracking.shared.error(message: "SDK push event error, systemEventTypes is empty!"); YXTrackingSQLiteManager.addTrackingData(data: YXTracking.shared.eventRequestResult) return } // 打印日志信息 YXTracking.shared.log(message: "SDK add event success\nEvent Request Parameter:\n\(YXTracking.shared.eventRequestResult)"); // 缓存事件数据或立即推送事件数据 if pushNow { YXTracking.shared.pushEvent(event: YXTracking.shared.eventRequestResult) } else { YXTrackingSQLiteManager.addTrackingData(data: YXTracking.shared.eventRequestResult) YXTracking.shared.checkEventCount() } } /// 设置用户信息 /// - Parameters: /// - userInfo: 用户信息 /// - Returns: 无返回值 public class func setUserInfo(userId: Int64, userName: String, account: String) { let userInfo = UserInfo(userId: userId, userName: userName, account: account) YXTracking.shared.eventRequestResult.userInfo = userInfo YXTracking.shared.log(message: "SDK config user info success\nUser info:\n\(userInfo)"); } /// 清除用户信息 public class func clearUserInfo() { YXTracking.shared.eventRequestResult.userInfo = nil YXTracking.shared.log(message: "SDK clear user info success!"); } /// 获取设备信息 func getDeviceInfo() { let device = UIDevice.current let os = "\(device.systemName) \(device.systemVersion)" let localizedModel = device.localizedModel let screenWidthPoints = UIScreen.main.bounds.width let screenHeightPoints = UIScreen.main.bounds.height let scale = UIScreen.main.scale let screenWidthPixels = screenWidthPoints * scale let screenHeightPixels = screenHeightPoints * scale let screenResolution = "\(Int(screenWidthPixels))x\(Int(screenHeightPixels))" let deviceInfo = DeviceInfo(os: os, model: localizedModel, screenResolution: screenResolution) YXTracking.shared.eventRequestResult.deviceInfo = deviceInfo YXTracking.shared.debug(message: "SDK config device info success\nDevice info:\n\(deviceInfo)"); } /// 获取时间戳 func getTimestamp() { let currentDate = Date() let unixTimestamp = currentDate.timeIntervalSince1970 let milliseconds = Int64(unixTimestamp * 1000) let customFormatter = ISO8601DateFormatter() customFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] customFormatter.timeZone = TimeZone.current let isoStringWithMilliseconds = customFormatter.string(from: currentDate) YXTracking.shared.eventRequestResult.clientTimestamp = milliseconds YXTracking.shared.eventRequestResult.timestamp = isoStringWithMilliseconds } /// 判断本地数量是否达到缓存上限,如已达到,则推送缓存事件 func checkEventCount() { let count = YXTrackingSQLiteManager.getTrackingDataCount() if count >= cacheCount { YXTracking.pushEventList() } } /// 开启定时推送本地缓存的事件日志列表 func startThreadPool() { let queue = DispatchQueue(label: "com.yx.tracking", qos: .userInitiated) queue.async { [weak self] in guard let self = self else { return } while true { YXTracking.shared.log(message: "SDK push event list on thread pool!") YXTracking.pushEventList() Thread.sleep(forTimeInterval: TimeInterval(pushInterval)) } } } /// 根据系统编码获取系统所有维度信息 func getSystemAllDimInfo() { let systemCode = YXTracking.shared.eventRequestResult.system_code ?? "" guard !systemCode.isEmpty else { return } let urlString = "\(host)/api/ExternalEventlogs/GetSystemAllDimInfo?systemCode=\(systemCode)" guard let url = URL(string: urlString) else { return } let task = URLSession.shared.dataTask(with: url) { [self] data, response, error in if let error = error { YXTracking.shared.error(message: error.localizedDescription) return } guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) if let dictionary = json as? [String: Any] { if let code = dictionary["code"] as? Int, code == 200 { if let result = dictionary["data"] as? Dictionary, let resultData = try? JSONSerialization.data(withJSONObject: result) { do { let resultModel = try JSONDecoder().decode(SystemAllDimInfoResponseResult.self, from: resultData) systemAllDimInfoResponseResult = resultModel YXTracking.shared.log(message: "SDK init success\nSystem All Dim Info:\n\(systemAllDimInfoResponseResult)"); // 初始化成功后 推送本地缓存的事件日志列表 YXTracking.pushEventList() // 开启线程池,定时推送本地缓存的事件日志列表 YXTracking.shared.startThreadPool() } catch { YXTracking.shared.error(message: "SDK init fail!"); } } else { YXTracking.shared.error(message: "无法创建Data对象!") } } else { YXTracking.shared.error(message: "SDK init fail,error code: \(dictionary["code"] ?? "未知")") } } } task.resume() } /// 推送单条日志 /// - Parameters: /// - event: 事件对象 /// - Returns: 无返回值 func pushEvent(event: EventRequestResult) { let urlString = "\(host)/api/ExternalEventlogs/AddEventLog" guard let url = URL(string: urlString) else { return } // 创建URL请求 var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") // 将event对象转换为JSON数据 let jsonEncoder = JSONEncoder() do { YXTracking.shared.log(message: "SDK push event:\n\(event)"); let jsonData = try jsonEncoder.encode(event) request.httpBody = jsonData } catch { YXTracking.shared.error(message: "SDK push event error encoding\n\(error)"); return } // 发送请求 let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { YXTracking.shared.error(message: "SDK push event error sending\n\(error)"); return } guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) if let dictionary = json as? [String: Any] { if let code = dictionary["code"] as? Int, code == 200 { YXTracking.shared.success(message: "SDK push event success Response:\n\(String(data: data, encoding: .utf8) ?? "")"); } else { YXTrackingSQLiteManager.addTrackingData(data: YXTracking.shared.eventRequestResult) YXTracking.shared.error(message: "SDK push event fail Response:\n\(String(data: data, encoding: .utf8) ?? "")"); } } } task.resume() } /// 推送多条日志 public class func pushEventList() { /// 判断是否正在进行pushEventList 防止重复推送 if YXTracking.shared.isPushingEventList { return } YXTracking.shared.isPushingEventList = true /// 获取本地缓存的事件日志列表数量 let count = YXTrackingSQLiteManager.getTrackingDataCount() guard count > 0 else { return } /// 组装请求URL let urlString = "\(YXTracking.shared.host)/api/ExternalEventlogs/AddEventListLog" guard let url = URL(string: urlString) else { return } /// 创建URL请求 var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") /// 读取本地数据 let eventList = YXTrackingSQLiteManager.readTrackingData() var events: [EventRequestResult] = [] for item in eventList { events.append(item) } /// 判断是否为空列表,不为空则继续推送 guard !events.isEmpty else { return } /// 将event list对象转换为JSON数据 let jsonEncoder = JSONEncoder() do { let jsonData = try jsonEncoder.encode(events) request.httpBody = jsonData } catch { YXTracking.shared.error(message: "SDK push event list error encoding\n\(error)"); YXTracking.shared.isPushingEventList = false return } /// 发送请求 let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { YXTracking.shared.error(message: "SDK push event list error sending\n\(error)"); YXTracking.shared.isPushingEventList = false return } guard let data = data else { YXTracking.shared.isPushingEventList = false return } let json = try? JSONSerialization.jsonObject(with: data, options: []) if let dictionary = json as? [String: Any] { if let code = dictionary["code"] as? Int, code == 200 { YXTrackingSQLiteManager.deleteTrackingData() YXTracking.shared.success(message: "SDK push event list success Response:\n\(String(data: data, encoding: .utf8) ?? "")") } else { YXTracking.shared.error(message: "SDK push event list fail Response:\n\(String(data: data, encoding: .utf8) ?? "")") } } YXTracking.shared.isPushingEventList = false } task.resume() } /// 监听网络变化 func addNetworkReachabilityObserver() { let monitor = NWPathMonitor() let queue = DispatchQueue(label: "NetworkMonitor") monitor.pathUpdateHandler = { path in let systemEventTypes = YXTracking.shared.systemAllDimInfoResponseResult.systemEventTypes ?? [] guard systemEventTypes.isEmpty else { return } if path.status == .satisfied { YXTracking.shared.log(message: "SDK network connected!"); YXTracking.shared.getSystemAllDimInfo() } else { YXTracking.shared.log(message: "SDK network not connected!"); } } monitor.start(queue: queue) } /// 监听应用切换到后台 func addEnterBackgroundNotificationObserver() { NotificationCenter.default.addObserver(self, selector: #selector(onPushEventList), name: UIApplication.didEnterBackgroundNotification, object: nil) } /// 监听系统即将终止应用程序 func addWillTerminateNotificationObserver() { NotificationCenter.default.addObserver(self, selector: #selector(onPushEventList), name: UIApplication.willTerminateNotification, object: nil) } /// 监听回调:推送事件列表 @objc func onPushEventList() { YXTracking.pushEventList() } } /// 扩展 extension YXTracking { /// 日志打印 func log(message: String) { guard YXTracking.shared.enableLog else { return } print("ℹ️ [YXTracking LOG] \(message)") } func debug(message: String) { guard YXTracking.shared.enableLog else { return } print("🐛 [YXTracking DEBUG] \(message)") } func warning(message: String) { guard YXTracking.shared.enableLog else { return } print("⚠️ [YXTracking WARNING] \(message)") } func success(message: String) { guard YXTracking.shared.enableLog else { return } print("✅ [YXTracking SUCCESS] \(message)") } func error(message: String) { guard YXTracking.shared.enableLog else { return } print("❌ [YXTracking ERROR] \(message)") } func fatal(message: String) { guard YXTracking.shared.enableLog else { return } print("💀 [YXTracking FATAL] \(message)") } }