diff --git a/YXTrackingSDK/YXTracking.swift b/YXTrackingSDK/YXTracking.swift index e8ebb17..06b3cb1 100644 --- a/YXTrackingSDK/YXTracking.swift +++ b/YXTrackingSDK/YXTracking.swift @@ -6,12 +6,14 @@ // import UIKit +import Network /// 远轩埋点SDK实现类 四种上报机制 /// 1、初始化时触发:用户在初始化SDK时,会将缓存的事件数据上报。 /// 2、定时/定量触发:数据条数达到阈值(默认20条)或定时器到期(默认600秒)时会将缓存的事件数据上报。 /// 3、事件触发:用户在添加事件时可选择立即上报。 /// 4、应用进入后台和即将被系统终止时触发:用户在应用内停留一段时间后,手动切换到其他应用或桌面时,或即将被系统终止时,会将缓存的事件数据上报。 +/// 5、网络状态变化时触发:当应用网络状态发生变化时,会将缓存的事件数据上报。 public class YXTracking: NSObject { static let shared = YXTracking() @@ -28,6 +30,8 @@ public class YXTracking: NSObject { private var systemAllDimInfoResponseResult: SystemAllDimInfoResponseResult = SystemAllDimInfoResponseResult() // 系统所有维度信息 + private var isPushingEventList: Bool = false // 是否正在推送事件列表 + /// 初始化SDK /// - Parameters: /// - host: 主机地址 @@ -63,6 +67,9 @@ public class YXTracking: NSObject { // 打印日志信息 YXTracking.shared.log(message: "SDK config success!"); + // 添加网络状态变化通知监听 网络状态发生变化时触发缓存事件推送 + YXTracking.shared.addNetworkReachabilityObserver() + // 添加应用进入后台和即将被系统终止的通知监听 应用进入后台时或即将被系统终止时触发缓存事件推送 YXTracking.shared.addEnterBackgroundNotificationObserver() YXTracking.shared.addWillTerminateNotificationObserver() @@ -87,7 +94,22 @@ public class YXTracking: NSObject { /// - customTags: 自定义标签数组(可选) /// - pushNow: 是否立即推送 /// - Returns: 无返回值 - public class func addEvent(eventType: String, page: String, buttonId: String, url: String, customTags: Array?, pushNow: Bool) { + 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 { @@ -102,22 +124,10 @@ public class YXTracking: NSObject { } } else { YXTracking.shared.error(message: "SDK push event error, systemEventTypes is empty!"); + YXTrackingSQLiteManager.addTrackingData(data: YXTracking.shared.eventRequestResult) return } - // 重置事件请求参数中的事件类型 - 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 success\nEvent Request Parameter:\n\(YXTracking.shared.eventRequestResult)"); @@ -141,7 +151,7 @@ public class YXTracking: NSObject { YXTracking.shared.log(message: "SDK config user info success\nUser info:\n\(userInfo)"); } - /// 清空用户信息 + /// 清除用户信息 public class func clearUserInfo() { YXTracking.shared.eventRequestResult.userInfo = nil @@ -185,7 +195,7 @@ public class YXTracking: NSObject { YXTracking.shared.eventRequestResult.timestamp = isoStringWithMilliseconds } - /// 判断数据库数量是否达到缓存上限,如已达到,则推送缓存事件 + /// 判断本地数量是否达到缓存上限,如已达到,则推送缓存事件 func checkEventCount() { let count = YXTrackingSQLiteManager.getTrackingDataCount() if count >= cacheCount { @@ -196,10 +206,12 @@ public class YXTracking: NSObject { /// 开启定时推送本地缓存的事件日志列表 func startThreadPool() { let queue = DispatchQueue(label: "com.yx.tracking", qos: .userInitiated) - queue.async { [self] in + 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() - sleep(UInt32(pushInterval)) + Thread.sleep(forTimeInterval: TimeInterval(pushInterval)) } } } @@ -272,6 +284,8 @@ public class YXTracking: NSObject { // 将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 { @@ -306,44 +320,56 @@ public class YXTracking: NSObject { /// 推送多条日志 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 - } + guard let url = URL(string: urlString) else { return } - // 创建URL请求 + /// 创建URL请求 var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") - // 读取数据库数据 + /// 读取本地数据 let eventList = YXTrackingSQLiteManager.readTrackingData() - var event: [EventRequestResult] = [] + var events: [EventRequestResult] = [] for item in eventList { - event.append(item) + events.append(item) } + + /// 判断是否为空列表,不为空则继续推送 + guard !events.isEmpty else { return } - // 将event list对象转换为JSON数据 + /// 将event list对象转换为JSON数据 let jsonEncoder = JSONEncoder() do { - let jsonData = try jsonEncoder.encode(event) + 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 } @@ -351,27 +377,47 @@ public class YXTracking: NSObject { 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) ?? "")"); + YXTracking.shared.success(message: "SDK push event list success Response:\n\(String(data: data, encoding: .utf8) ?? "")") } else { - YXTracking.shared.error(message: "SDK push event fail Response:\n\(String(data: data, encoding: .utf8) ?? "")"); + 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() } diff --git a/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate b/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate index b90b5ed..0afd61e 100644 Binary files a/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate and b/YXTrackingSDK/YXTrackingSDK.xcodeproj/project.xcworkspace/xcuserdata/a1234.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift b/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift index be048e8..a125743 100644 --- a/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift +++ b/YXTrackingSDK/YXTrackingSDK/YXTrackingSDK.swift @@ -38,7 +38,7 @@ public struct EventRequestResult: Codable { var timestamp: String? // 事件发生时间(ISO8601格式) var deviceInfo: DeviceInfo? // 设备信息 var eventParams: EventParams? // 事件参数 - var customTags: Array? // 自定义标签(可选) + var customTags: Dictionary? // 自定义标签(可选) } /// 定义一个结构体来存储系统信息 diff --git a/YXTrackingSDK/YXTrackingSQLiteManager.swift b/YXTrackingSDK/YXTrackingSQLiteManager.swift index 32a3c86..3111902 100644 --- a/YXTrackingSDK/YXTrackingSQLiteManager.swift +++ b/YXTrackingSDK/YXTrackingSQLiteManager.swift @@ -10,210 +10,66 @@ import SQLite3 class YXTrackingSQLiteManager: NSObject { - // 添加跟踪数据到数据库 - class func addTrackingData(data: EventRequestResult) { - // 创建数据库连接 - let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! - let path = URL(fileURLWithPath: dbPath).appendingPathComponent("tracking.sqlite").path - var db: OpaquePointer? = nil - if sqlite3_open(path, &db) != SQLITE_OK { - YXTracking.shared.error(message: "SDK sqlite open failed!"); - return - } + static let shared = YXTrackingSQLiteManager() + + /// 文件路径管理 + private let fileManager = FileManager.default - YXTracking.shared.success(message: "SDK sqlite open success!"); + /// 文档目录路径 + var documentsDirectory: URL { + fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + } - // 创建表 - let createTableSQL = """ - CREATE TABLE IF NOT EXISTS tracking_data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - system_code TEXT, - event_type TEXT, - user_info BLOB, - client_type INT, - client_timestamp INT64, - timestamp TEXT, - device_info BLOB, - event_params BLOB, - custom_tags BLOB - ) - """ - if sqlite3_exec(db, createTableSQL, nil, nil, nil) != SQLITE_OK { - YXTracking.shared.error(message: "SDK sqlite create table failed!"); - return - } - - YXTracking.shared.success(message: "SDK sqlite create table success!"); - - // 插入数据 - let encoder = JSONEncoder() - let insertSQL = "INSERT INTO tracking_data (system_code, event_type, user_info, client_type, client_timestamp, timestamp, device_info, event_params, custom_tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" - var statement: OpaquePointer? = nil - if sqlite3_prepare_v2(db, insertSQL, -1, &statement, nil) == SQLITE_OK { - let system_code = data.system_code ?? "" - let event_type = data.eventType ?? "" - let user_info = (data.userInfo != nil) ? try! encoder.encode(data.userInfo) : nil - let client_type = data.clientType ?? 0 - let client_timestamp = data.clientTimestamp ?? 0 - let timestamp = data.timestamp ?? "" - let device_info = try! encoder.encode(data.deviceInfo) - let event_params = try! encoder.encode(data.eventParams) - let custom_tags = try! JSONSerialization.data(withJSONObject: data.customTags ?? [], options: []) - if sqlite3_bind_text(statement, 1, system_code, -1, nil) != SQLITE_OK || sqlite3_bind_text(statement, 2, event_type, -1, nil) != SQLITE_OK || ((data.userInfo != nil) ? (sqlite3_bind_blob(statement, 3, user_info! as CFTypeRef as? UnsafeRawPointer, Int32(user_info!.count), nil) != SQLITE_OK) : (sqlite3_bind_null(statement, 3) != SQLITE_OK)) || sqlite3_bind_int(statement, 4, client_type) != SQLITE_OK || sqlite3_bind_int64(statement, 5, client_timestamp) != SQLITE_OK || sqlite3_bind_text(statement, 6, timestamp, -1, nil) != SQLITE_OK || sqlite3_bind_blob(statement, 7, device_info as CFTypeRef as? UnsafeRawPointer, Int32(device_info.count), nil) != SQLITE_OK || sqlite3_bind_blob(statement, 8, event_params as CFTypeRef as? UnsafeRawPointer, Int32(event_params.count), nil) != SQLITE_OK || sqlite3_bind_blob(statement, 9, custom_tags as CFTypeRef as? UnsafeRawPointer, Int32(custom_tags.count), nil) != SQLITE_OK { - YXTracking.shared.error(message: "SDK sqlite bind parameter failed!"); - return - } else if sqlite3_step(statement) != SQLITE_DONE { - YXTracking.shared.error(message: "SDK sqlite insert data failed!"); - return - } - - YXTracking.shared.success(message: "SDK sqlite bind parameter success!"); - } else { - YXTracking.shared.error(message: "SDK sqlite preparing to insert data failed!"); - return - } - - YXTracking.shared.success(message: "SDK sqlite insert data success!"); - - // 关闭数据库连接 - do { - sqlite3_close(db) - } - + /// 文件路径 + var fileURL: URL { + documentsDirectory.appendingPathComponent("tracking.json") } - // 读取跟踪数据 + /// JSON编码器 + let encoder = JSONEncoder() + + /// JSON解码器 + let decoder = JSONDecoder() + + /// 添加跟踪数据到文件 + class func addTrackingData(data: EventRequestResult) { + var trackingData: [EventRequestResult] = [] + trackingData.append(contentsOf: readTrackingData()) + trackingData.append(data) + + let data = try! YXTrackingSQLiteManager.shared.encoder.encode(trackingData) + try! data.write(to: YXTrackingSQLiteManager.shared.fileURL, options: .atomic) + } + + /// 读取跟踪数据 class func readTrackingData() -> [EventRequestResult] { var trackingData: [EventRequestResult] = [] - // 创建数据库连接 - let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! - let path = URL(fileURLWithPath: dbPath).appendingPathComponent("tracking.sqlite").path - var db: OpaquePointer? = nil - if sqlite3_open(path, &db) != SQLITE_OK { - YXTracking.shared.error(message: "SDK sqlite open failed!"); + /// 判断fileURL是否存在 + guard YXTrackingSQLiteManager.shared.fileManager.fileExists(atPath: YXTrackingSQLiteManager.shared.fileURL.path) else { return trackingData } - // 查询数据 - let decoder = JSONDecoder() - let querySQL = "SELECT system_code, event_type, user_info, client_type, client_timestamp, timestamp, device_info, event_params, custom_tags FROM tracking_data" - var statement: OpaquePointer? = nil - if sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK { - while (sqlite3_step(statement) == SQLITE_ROW) { - // 获取数据并转换为EventRequestResult - let system_code = String(cString: sqlite3_column_text(statement, 1)) - let event_type = String(cString: sqlite3_column_text(statement, 2)) - var user_info = UserInfo(userId: nil, userName: nil, account: nil) - if sqlite3_column_type(statement, 3) != SQLITE_NULL { - if let user_info_blob = sqlite3_column_blob(statement, 3) { - let user_info_blob_length = sqlite3_column_bytes(statement, 3) - let data = Data(bytes: user_info_blob, count: Int(user_info_blob_length)) - user_info = try! decoder.decode(UserInfo.self, from: data) - } - } - let client_type = sqlite3_column_int(statement, 4) - let client_timestamp = sqlite3_column_int64(statement, 5) - let timestamp = String(cString: sqlite3_column_text(statement, 6)) - var device_info = DeviceInfo(os: nil, model: nil, screenResolution: nil) - if sqlite3_column_type(statement, 7) != SQLITE_NULL { - if let device_info_blob = sqlite3_column_blob(statement, 7) { - let device_info_blob_length = sqlite3_column_bytes(statement, 7) - let data = Data(bytes: device_info_blob, count: Int(device_info_blob_length)) - device_info = try! decoder.decode(DeviceInfo.self, from: data) - } - } - var event_params = EventParams(page: nil, buttonId: nil, url: nil) - if sqlite3_column_type(statement, 8) != SQLITE_NULL { - if let event_params_blob = sqlite3_column_blob(statement, 8) { - let event_params_blob_length = sqlite3_column_bytes(statement, 8) - let data = Data(bytes: event_params_blob, count: Int(event_params_blob_length)) - event_params = try! decoder.decode(EventParams.self, from: data) - } - } - var custom_tags = Array() - if sqlite3_column_type(statement, 9) != SQLITE_NULL { - if let custom_tags_blob = sqlite3_column_blob(statement, 9) { - let custom_tags_blob_length = sqlite3_column_bytes(statement, 9) - let data = Data(bytes: custom_tags_blob, count: Int(custom_tags_blob_length)) - custom_tags = try! decoder.decode([String].self, from: data) - } - } - - let event = EventRequestResult(system_code: system_code, - eventType: event_type, - userInfo: user_info, - clientType: client_type, - clientTimestamp: client_timestamp, - timestamp: timestamp, - deviceInfo: device_info, - eventParams: event_params, - customTags: custom_tags) - trackingData.append(event) - } - sqlite3_finalize(statement) - } else { - YXTracking.shared.error(message: "SDK sqlite read data failed!"); - } - - // 关闭数据库连接 - do { - sqlite3_close(db) - } + let data = try! Data(contentsOf: YXTrackingSQLiteManager.shared.fileURL) + let array = try! YXTrackingSQLiteManager.shared.decoder.decode([EventRequestResult].self, from: data) + trackingData.append(contentsOf: array) return trackingData } - // 删除跟踪数据 + /// 删除跟踪数据 class func deleteTrackingData() { - // 创建数据库连接 - let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! - let path = URL(fileURLWithPath: dbPath).appendingPathComponent("tracking.sqlite").path - var db: OpaquePointer? = nil - if sqlite3_open(path, &db) != SQLITE_OK { - YXTracking.shared.error(message: "SDK sqlite open failed!"); + guard YXTrackingSQLiteManager.shared.fileManager.fileExists(atPath: YXTrackingSQLiteManager.shared.fileURL.path) else { + YXTracking.shared.debug(message: "SDK tracking data file not exist!") return } - - // 删除数据 - let deleteSQL = "DELETE FROM tracking_data" - if sqlite3_exec(db, deleteSQL, nil, nil, nil) != SQLITE_OK { - YXTracking.shared.error(message: "SDK sqlite delete data failed!"); - } - - // 关闭数据库连接 - do { - sqlite3_close(db) - } + try! YXTrackingSQLiteManager.shared.fileManager.removeItem(at: YXTrackingSQLiteManager.shared.fileURL) + YXTracking.shared.debug(message: "SDK tracking data file deleted!") } - // 获取数据库缓存条数 + /// 获取数据库缓存条数 class func getTrackingDataCount() -> Int { - // 创建数据库连接 - let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! - let path = URL(fileURLWithPath: dbPath).appendingPathComponent("tracking.sqlite").path - var db: OpaquePointer? = nil - if sqlite3_open(path, &db) != SQLITE_OK { - YXTracking.shared.error(message: "SDK sqlite open failed!"); - return -1 - } - - // 查询数据 - var dataCount = -1 - let querySQL = "SELECT COUNT(*) FROM tracking_data" - var statement: OpaquePointer? = nil - if sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK { - sqlite3_step(statement) - dataCount = Int(sqlite3_column_int64(statement, 0)) - sqlite3_finalize(statement) - } else { - YXTracking.shared.error(message: "SDK sqlite read data failed!"); - } - - // 关闭数据库连接 - do { - sqlite3_close(db) - } - - return dataCount + var trackingData: [EventRequestResult] = [] + trackingData.append(contentsOf: readTrackingData()) + return trackingData.count } }