462 lines
18 KiB
Swift
462 lines
18 KiB
Swift
//
|
||
// 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<String, String>?, 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<String, Any>,
|
||
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)")
|
||
}
|
||
|
||
}
|