|
| 1 | +// |
| 2 | +// KeyMetrics.swift |
| 3 | +// TUICallKit |
| 4 | +// |
| 5 | +// Created by yukiwwwang on 2026/01/06. |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | +import TUICore |
| 10 | +import AtomicXCore |
| 11 | + |
| 12 | +#if canImport(TXLiteAVSDK_TRTC) |
| 13 | +import TXLiteAVSDK_TRTC |
| 14 | +#elseif canImport(TXLiteAVSDK_Professional) |
| 15 | +import TXLiteAVSDK_Professional |
| 16 | +#endif |
| 17 | + |
| 18 | +class KeyMetrics { |
| 19 | + private static let TAG = "KeyMetrics" |
| 20 | + private static let API_REPORT_ROOM_ENGINE_EVENT = "reportRoomEngineEvent" |
| 21 | + private static var lastReportedCallId: String = "" |
| 22 | + |
| 23 | + enum EventId: Int { |
| 24 | + case received = 171010 |
| 25 | + case wakeup = 171011 |
| 26 | + } |
| 27 | + |
| 28 | + static func countUV(eventId: EventId, callId: String) { |
| 29 | + let isDuplicateCall = (callId == lastReportedCallId) && !callId.isEmpty |
| 30 | + if eventId == .received && !callId.isEmpty { |
| 31 | + lastReportedCallId = callId |
| 32 | + } |
| 33 | + |
| 34 | + if isDuplicateCall && eventId == .received { |
| 35 | + Logger.info("\(TAG) skip duplicate report for callId: \(callId)") |
| 36 | + return |
| 37 | + } |
| 38 | + |
| 39 | + switch eventId { |
| 40 | + case .received: |
| 41 | + countEvent(eventId: eventId, callId: callId) |
| 42 | + case .wakeup: |
| 43 | + if callId == lastReportedCallId { |
| 44 | + return |
| 45 | + } |
| 46 | + lastReportedCallId = callId |
| 47 | + countEvent(eventId: eventId, callId: callId) |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + static func reset() { |
| 52 | + lastReportedCallId = "" |
| 53 | + } |
| 54 | + |
| 55 | + static func flushMetrics() { |
| 56 | + var paramsJson: [String: Any] = [:] |
| 57 | + paramsJson["sdkAppId"] = TUILogin.getSdkAppID() |
| 58 | + paramsJson["report"] = "report" |
| 59 | + |
| 60 | + var jsonParams: [String: Any] = [:] |
| 61 | + jsonParams["api"] = "KeyMetricsStats" |
| 62 | + jsonParams["params"] = paramsJson |
| 63 | + |
| 64 | + if let jsonData = try? JSONSerialization.data(withJSONObject: jsonParams), |
| 65 | + let jsonString = String(data: jsonData, encoding: .utf8) { |
| 66 | + TRTCCloud.sharedInstance().callExperimentalAPI(jsonString) |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + private static func countEvent(eventId: EventId, callId: String) { |
| 71 | + trackForKibana(eventId: eventId, callId: callId) |
| 72 | + trackForTRTC(eventId: eventId) |
| 73 | + } |
| 74 | + |
| 75 | + private static func trackForKibana(eventId: EventId, callId: String) { |
| 76 | + do { |
| 77 | + let extensionJson = buildExtensionJson(callId: callId) |
| 78 | + let extensionString = String(data: try JSONSerialization.data(withJSONObject: extensionJson), encoding: .utf8) ?? "" |
| 79 | + let payload = buildEventPayload(eventId: eventId, extensionMessage: extensionString) |
| 80 | + |
| 81 | + if let payloadData = try? JSONSerialization.data(withJSONObject: payload), |
| 82 | + let payloadString = String(data: payloadData, encoding: .utf8) { |
| 83 | + V2TIMManager.sharedInstance()?.callExperimentalAPI(api: API_REPORT_ROOM_ENGINE_EVENT, |
| 84 | + param: payloadString as NSObject, |
| 85 | + succ: {_ in |
| 86 | + Logger.info("\(TAG) trackForKibana success: eventId=\(eventId)") |
| 87 | + }, fail: { code, desc in |
| 88 | + Logger.error("\(TAG) trackForKibana failed: code=\(code), desc=\(desc ?? "")") |
| 89 | + }) |
| 90 | + } |
| 91 | + } catch { |
| 92 | + Logger.error("\(TAG) trackForKibana exception: eventId=\(eventId), error=\(error)") |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + private static func trackForTRTC(eventId: EventId) { |
| 97 | + var paramsJson: [String: Any] = [:] |
| 98 | + paramsJson["opt"] = "CountPV" |
| 99 | + paramsJson["key"] = eventId.rawValue |
| 100 | + paramsJson["withInstanceTrace"] = false |
| 101 | + paramsJson["version"] = TUICALL_VERSION |
| 102 | + |
| 103 | + var jsonParams: [String: Any] = [:] |
| 104 | + jsonParams["api"] = "KeyMetricsStats" |
| 105 | + jsonParams["params"] = paramsJson |
| 106 | + |
| 107 | + if let jsonData = try? JSONSerialization.data(withJSONObject: jsonParams), |
| 108 | + let jsonString = String(data: jsonData, encoding: .utf8) { |
| 109 | + TRTCCloud.sharedInstance().callExperimentalAPI(jsonString) |
| 110 | + } |
| 111 | + |
| 112 | + if TUILogin.getSdkAppID() > 0 { |
| 113 | + flushMetrics() |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + private static func buildExtensionJson(callId: String) -> [String: Any] { |
| 118 | + var json: [String: Any] = [:] |
| 119 | + |
| 120 | + let activeCall = CallStore.shared.state.value.activeCall |
| 121 | + |
| 122 | + // Basic Info |
| 123 | + json[JsonKeys.callId] = callId |
| 124 | + json[JsonKeys.intRoomId] = Int(activeCall.roomId) ?? 0 |
| 125 | + json[JsonKeys.strRoomId] = activeCall.roomId |
| 126 | + json[JsonKeys.uiKitVersion] = TUICALL_VERSION |
| 127 | + |
| 128 | + // Platform Info |
| 129 | + json[JsonKeys.platform] = "ios" |
| 130 | + json[JsonKeys.framework] = FrameworkConstants.framework |
| 131 | + json[JsonKeys.deviceBrand] = "Apple" |
| 132 | + json[JsonKeys.deviceModel] = getDeviceModel() |
| 133 | + json[JsonKeys.iosVersion] = UIDevice.current.systemVersion |
| 134 | + |
| 135 | + return json |
| 136 | + } |
| 137 | + |
| 138 | + private static func buildEventPayload(eventId: EventId, extensionMessage: String) -> [String: Any] { |
| 139 | + var json: [String: Any] = [:] |
| 140 | + json[JsonKeys.eventId] = eventId.rawValue |
| 141 | + json[JsonKeys.eventCode] = 0 |
| 142 | + json[JsonKeys.eventResult] = 0 |
| 143 | + json[JsonKeys.eventMessage] = TUICALL_VERSION |
| 144 | + json[JsonKeys.moreMessage] = "" |
| 145 | + json[JsonKeys.extensionMessage] = extensionMessage |
| 146 | + return json |
| 147 | + } |
| 148 | + |
| 149 | + private static func getDeviceModel() -> String { |
| 150 | + var systemInfo = utsname() |
| 151 | + uname(&systemInfo) |
| 152 | + let machineMirror = Mirror(reflecting: systemInfo.machine) |
| 153 | + let identifier = machineMirror.children.reduce("") { identifier, element in |
| 154 | + guard let value = element.value as? Int8, value != 0 else { return identifier } |
| 155 | + return identifier + String(UnicodeScalar(UInt8(value))) |
| 156 | + } |
| 157 | + return identifier |
| 158 | + } |
| 159 | + |
| 160 | + private struct JsonKeys { |
| 161 | + // Event Payload Keys |
| 162 | + static let eventId = "event_id" |
| 163 | + static let eventCode = "event_code" |
| 164 | + static let eventResult = "event_result" |
| 165 | + static let eventMessage = "event_message" |
| 166 | + static let moreMessage = "more_message" |
| 167 | + static let extensionMessage = "extension_message" |
| 168 | + |
| 169 | + // Basic Info Keys |
| 170 | + static let callId = "call_id" |
| 171 | + static let intRoomId = "int_room_id" |
| 172 | + static let strRoomId = "str_room_id" |
| 173 | + static let uiKitVersion = "ui_kit_version" |
| 174 | + |
| 175 | + // Platform Info Keys |
| 176 | + static let platform = "platform" |
| 177 | + static let framework = "framework" |
| 178 | + static let deviceBrand = "device_brand" |
| 179 | + static let deviceModel = "device_model" |
| 180 | + static let iosVersion = "ios_version" |
| 181 | + static let isForeground = "is_foreground" |
| 182 | + static let isScreenLocked = "is_screen_locked" |
| 183 | + } |
| 184 | +} |
0 commit comments