From 679c6c0af50e777f14f9adce90342a332850b699 Mon Sep 17 00:00:00 2001 From: Cui Hongwei Date: Tue, 28 Jul 2026 09:32:25 +0800 Subject: [PATCH] feat: add qingan dashboard telemetry --- src/views/qingAn.vue | 421 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 411 insertions(+), 10 deletions(-) diff --git a/src/views/qingAn.vue b/src/views/qingAn.vue index a140b36..bb7147d 100644 --- a/src/views/qingAn.vue +++ b/src/views/qingAn.vue @@ -412,6 +412,26 @@ export default { deviceHistoryRequesting: false, equipmentStatusTimer: null, equipmentStatusRequesting: false, + telemetryWriteType: '12', + telemetryAppId: 'qingan-dt-tv', + telemetryAppVersion: '0.1.0', + telemetryEnv: 'prod', + telemetryClientIp: '192.168.1.107', + telemetrySessionId: '', + telemetryPageTraceId: '', + telemetryStartAt: 0, + telemetryIframeStartAt: 0, + telemetryIframeMode: '', + telemetryIframeUrl: '', + telemetryIframeTimeoutTimer: null, + telemetryErrorHandler: null, + telemetryPromiseHandler: null, + telemetrySlowApiThreshold: 2000, + telemetryApiSuccessSampleRate: 0, + telemetryQueue: [], + telemetrySending: false, + telemetryReady: false, + telemetryThrottleMap: {}, clockTimer: null, resizeHandler: null, alarmPieChart: null, @@ -563,6 +583,7 @@ export default { this.srcDt = this.buildIframeUrl(config.dtIframe || this.srcDt, this.autoPlayCraftCode) this.srcSm = this.buildIframeUrl(this.smIframeBase, this.autoPlayCraftCode, this.selectedAutoPlayRate) this.titleText = config.titleText + this.initTelemetry(config) this.switchMode('DT') this.updateTime() this.setScale() @@ -598,6 +619,7 @@ export default { window.addEventListener('message', this.handleDmtMessage) }, beforeDestroy() { + this.destroyTelemetry() if (this.clockTimer) { clearInterval(this.clockTimer) this.clockTimer = null @@ -645,6 +667,355 @@ export default { } }, methods: { + initTelemetry(config = {}) { + const envFromBuild = typeof process !== 'undefined' && process.env ? process.env.NODE_ENV : '' + this.telemetryReady = true + this.telemetryEnv = config.telemetryEnv || config.env || (envFromBuild === 'development' ? 'dev' : 'prod') + this.telemetryAppVersion = config.telemetryAppVersion || config.appVersion || this.telemetryAppVersion + this.telemetrySessionId = this.getTelemetryStorageId('qinganTelemetrySessionId', 'sess') + this.telemetryPageTraceId = this.generateTelemetryId('trace') + this.telemetryStartAt = Date.now() + this.telemetryErrorHandler = (event) => { + if (event && event.target && event.target !== window) { + const target = event.target + const resourceUrl = target.src || target.href || '' + this.reportTelemetry('resource_error', 'exception', 'warn', { + tag_name: target.tagName || '', + resource_url: resourceUrl, + page_title: this.titleText || '' + }, { + success: false, + errorMessage: resourceUrl || '资源加载失败' + }) + return + } + const error = this.normalizeTelemetryError(event && event.error ? event.error : event) + this.reportTelemetry('js_error', 'exception', 'error', { + message: error.message, + filename: event && event.filename ? event.filename : '', + lineno: event && event.lineno ? event.lineno : '', + colno: event && event.colno ? event.colno : '', + stack: error.stack + }, { + success: false, + errorCode: error.name, + errorMessage: error.message + }) + } + this.telemetryPromiseHandler = (event) => { + const error = this.normalizeTelemetryError(event && event.reason ? event.reason : event) + this.reportTelemetry('promise_error', 'exception', 'error', { + message: error.message, + stack: error.stack + }, { + success: false, + errorCode: error.name, + errorMessage: error.message + }) + } + window.addEventListener('error', this.telemetryErrorHandler, true) + window.addEventListener('unhandledrejection', this.telemetryPromiseHandler) + this.reportTelemetry('page_start', 'frontend_event', 'info', { + title: this.titleText || '', + mode: this.currentMode, + screen: `${window.screen.width}x${window.screen.height}` + }, { success: true }) + this.$nextTick(() => { + const duration = Date.now() - this.telemetryStartAt + this.reportTelemetry('page_ready', 'performance', 'info', { + load_cost_ms: duration + }, { + durationMs: duration, + success: true + }) + }) + }, + destroyTelemetry() { + if (this.telemetryErrorHandler) { + window.removeEventListener('error', this.telemetryErrorHandler, true) + this.telemetryErrorHandler = null + } + if (this.telemetryPromiseHandler) { + window.removeEventListener('unhandledrejection', this.telemetryPromiseHandler) + this.telemetryPromiseHandler = null + } + if (this.telemetryIframeTimeoutTimer) { + clearTimeout(this.telemetryIframeTimeoutTimer) + this.telemetryIframeTimeoutTimer = null + } + this.telemetryReady = false + }, + getTelemetryStorageId(key, prefix) { + try { + const savedId = window.sessionStorage.getItem(key) + if (savedId) return savedId + const nextId = this.generateTelemetryId(prefix) + window.sessionStorage.setItem(key, nextId) + return nextId + } catch (error) { + console.warn('前端埋点 sessionStorage 不可用', error) + return this.generateTelemetryId(prefix) + } + }, + generateTelemetryId(prefix) { + return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 10)}` + }, + getTelemetryPagePath() { + if (this.$route && this.$route.fullPath) { + return this.$route.fullPath + } + if (window.location.hash) { + return window.location.hash + } + return window.location.pathname || '/qingAn' + }, + getTelemetryReferrer() { + return document.referrer || '-' + }, + getTelemetryUserId() { + return this.id || this.userId || 'anonymous' + }, + getTelemetryBrowserName(userAgent) { + if (/Edg\//.test(userAgent)) return 'Edge' + if (/Chrome\//.test(userAgent)) return 'Chrome' + if (/Firefox\//.test(userAgent)) return 'Firefox' + if (/Safari\//.test(userAgent)) return 'Safari' + return 'unknown' + }, + getTelemetryOsName(userAgent) { + if (/Windows/i.test(userAgent)) return 'Windows' + if (/Android/i.test(userAgent)) return 'Android' + if (/iPhone|iPad|iPod/i.test(userAgent)) return 'iOS' + if (/Mac OS/i.test(userAgent)) return 'macOS' + if (/Linux/i.test(userAgent)) return 'Linux' + return 'unknown' + }, + getTelemetryClientInfo() { + const userAgent = navigator.userAgent || '' + return { + browser: this.getTelemetryBrowserName(userAgent), + os: this.getTelemetryOsName(userAgent), + userAgent, + language: navigator.language || '', + screen: { + width: window.screen.width, + height: window.screen.height, + devicePixelRatio: window.devicePixelRatio || 1 + }, + viewport: { + width: window.innerWidth, + height: window.innerHeight + } + } + }, + getTelemetryCommonFields(traceId) { + return { + app_id: this.telemetryAppId, + app_version: this.telemetryAppVersion, + env: this.telemetryEnv, + page_path: this.getTelemetryPagePath(), + referrer: this.getTelemetryReferrer(), + trace_id: traceId || this.telemetryPageTraceId, + session_id: this.telemetrySessionId, + user_id: this.getTelemetryUserId(), + timestamp: Date.now(), + client_ip: this.telemetryClientIp, + client_info: this.getTelemetryClientInfo() + } + }, + normalizeTelemetryError(error) { + if (!error) { + return { + name: 'Error', + message: '未知错误', + stack: '' + } + } + if (typeof error === 'string') { + return { + name: 'Error', + message: error, + stack: '' + } + } + const response = error.response || {} + return { + name: error.name || response.statusText || 'Error', + message: error.message || response.statusText || String(error), + stack: error.stack || '', + status: response.status || '', + code: error.code || response.status || '' + } + }, + stringifyTelemetryValue(value, maxLength = 3800) { + let text = '' + try { + text = typeof value === 'string' ? value : JSON.stringify(value) + } catch (error) { + text = `无法序列化: ${error && error.message ? error.message : String(error)}` + } + if (!text) return '' + return text.length > maxLength ? text.slice(0, maxLength) : text + }, + canReportTelemetryEvent(key, interval = 10000) { + const now = Date.now() + const lastTime = this.telemetryThrottleMap[key] || 0 + if (now - lastTime < interval) { + return false + } + this.telemetryThrottleMap[key] = now + return true + }, + reportTelemetry(eventName, eventType = 'frontend_event', eventLevel = 'info', eventData = {}, options = {}) { + if (!this.telemetryReady && eventName !== 'page_start') { + return + } + const commonFields = this.getTelemetryCommonFields(options.traceId) + const payload = Object.assign({}, commonFields, { + event_type: eventType, + event_name: eventName, + event_level: eventLevel, + event_data: eventData || {}, + duration_ms: options.durationMs === undefined ? null : Number(options.durationMs), + success: options.success === undefined ? null : Boolean(options.success), + error_code: options.errorCode || '', + error_message: options.errorMessage || '' + }) + this.telemetryQueue.push(payload) + if (this.telemetryQueue.length > 100) { + this.telemetryQueue.splice(0, this.telemetryQueue.length - 100) + } + this.flushTelemetryQueue() + }, + flushTelemetryQueue() { + if (this.telemetrySending || !this.telemetryQueue.length) { + return + } + const payload = this.telemetryQueue.shift() + this.telemetrySending = true + const params = [ + ['@app_id', payload.app_id, 'string', '0'], + ['@app_version', payload.app_version, 'string', '0'], + ['@env', payload.env, 'string', '0'], + ['@page_path', payload.page_path, 'string', '0'], + ['@referrer', payload.referrer, 'string', '0'], + ['@trace_id', payload.trace_id, 'string', '0'], + ['@session_id', payload.session_id, 'string', '0'], + ['@user_id', payload.user_id, 'string', '0'], + ['@timestamp', String(payload.timestamp), 'string', '0'], + ['@client_ip', payload.client_ip, 'string', '0'], + ['@client_info', this.stringifyTelemetryValue(payload.client_info), 'string', '0'], + ['@event_type', payload.event_type, 'string', '0'], + ['@event_name', payload.event_name, 'string', '0'], + ['@event_level', payload.event_level, 'string', '0'], + ['@event_data', this.stringifyTelemetryValue(payload.event_data), 'string', '0'], + ['@duration_ms', payload.duration_ms === null ? '' : String(payload.duration_ms), 'string', '0'], + ['@success', payload.success === null ? '' : (payload.success ? '1' : '0'), 'string', '0'], + ['@error_code', payload.error_code, 'string', '0'], + ['@error_message', this.stringifyTelemetryValue(payload.error_message), 'string', '0'] + ] + const data = this.CreateData(this.telemetryWriteType, 'TV_前端埋点事件_写入', params) + this.ExecDatabase(data) + .catch((error) => { + if (this.canReportTelemetryEvent('telemetry_write_fail', 30000)) { + console.warn('TV_前端埋点事件_写入失败', error) + } + }) + .then(() => { + this.telemetrySending = false + this.flushTelemetryQueue() + }) + }, + createTelemetryApiTrace(procName, moduleName = '') { + return { + traceId: this.generateTelemetryId('api'), + startAt: Date.now(), + procName, + moduleName + } + }, + finishTelemetryApiTrace(trace, success, error, extra = {}) { + if (!trace) return + const duration = Date.now() - trace.startAt + const normalizedError = this.normalizeTelemetryError(error) + const shouldReport = !success || duration >= this.telemetrySlowApiThreshold || Math.random() < this.telemetryApiSuccessSampleRate + if (!shouldReport) return + const throttleKey = success ? `api_slow_${trace.procName}` : `api_fail_${trace.procName}` + if (!this.canReportTelemetryEvent(throttleKey, success ? 10000 : 15000)) { + return + } + this.reportTelemetry(success ? 'api_slow_query' : 'api_query_fail', success ? 'performance' : 'exception', success ? 'warn' : 'error', Object.assign({ + proc_name: trace.procName, + module_name: trace.moduleName, + duration_ms: duration + }, extra), { + traceId: trace.traceId, + durationMs: duration, + success, + errorCode: success ? '' : normalizedError.code || normalizedError.name, + errorMessage: success ? '' : normalizedError.message + }) + }, + execObservedDatabase(type, procName, params = [], meta = {}) { + const trace = this.createTelemetryApiTrace(procName, meta.moduleName || '') + const data = this.CreateData(type, procName, params) + return this.ExecDatabase(data) + .then((response) => { + const list = response && response.data ? response.data : null + this.finishTelemetryApiTrace(trace, true, null, { + row_count: Array.isArray(list) ? list.length : null + }) + return response + }) + .catch((error) => { + this.finishTelemetryApiTrace(trace, false, error) + throw error + }) + }, + startIframeTelemetry(mode) { + this.telemetryIframeStartAt = Date.now() + this.telemetryIframeMode = mode + this.telemetryIframeUrl = mode === 'SM' ? this.srcSm : this.srcDt + if (this.telemetryIframeTimeoutTimer) { + clearTimeout(this.telemetryIframeTimeoutTimer) + } + this.telemetryIframeTimeoutTimer = setTimeout(() => { + this.reportTelemetry('iframe_load_timeout', 'exception', 'warn', { + mode, + iframe_url: this.telemetryIframeUrl, + timeout_ms: 30000 + }, { + durationMs: Date.now() - this.telemetryIframeStartAt, + success: false, + errorMessage: 'iframe加载超时' + }) + this.telemetryIframeTimeoutTimer = null + }, 30000) + }, + reportIframeDomLoaded() { + if (!this.telemetryIframeStartAt) return + this.reportTelemetry('iframe_dom_load', 'performance', 'info', { + mode: this.telemetryIframeMode, + iframe_url: this.telemetryIframeUrl + }, { + durationMs: Date.now() - this.telemetryIframeStartAt, + success: true + }) + }, + reportIframeReady() { + if (!this.telemetryIframeStartAt) return + if (this.telemetryIframeTimeoutTimer) { + clearTimeout(this.telemetryIframeTimeoutTimer) + this.telemetryIframeTimeoutTimer = null + } + this.reportTelemetry('iframe_ready', 'performance', 'info', { + mode: this.telemetryIframeMode, + iframe_url: this.telemetryIframeUrl + }, { + durationMs: Date.now() - this.telemetryIframeStartAt, + success: true + }) + }, showLineTooltip(row, event) { this.detailTooltip.visible = true this.detailTooltip.type = 'line' @@ -843,8 +1214,8 @@ export default { loadStorageMatrixData() { if (this.storageMatrixRequesting) return this.storageMatrixRequesting = true - const data = this.CreateData('11', 'TV_物料库位矩阵_查询', []) - this.ExecDatabase(data) + const procName = 'TV_物料库位矩阵_查询' + this.execObservedDatabase('11', procName, [], { moduleName: '物料库位矩阵' }) .then((response) => { const list = response && response.data ? response.data : null if (this.isNoDataResult(list)) { @@ -914,8 +1285,8 @@ export default { loadAlarmEventData() { if (this.alarmEventRequesting) return this.alarmEventRequesting = true - const data = this.CreateData('11', 'TV_报警异常事件_查询', []) - this.ExecDatabase(data) + const procName = 'TV_报警异常事件_查询' + this.execObservedDatabase('11', procName, [], { moduleName: '报警与异常事件' }) .then((response) => { const list = response && response.data ? response.data : null if (this.isNoDataResult(list)) { @@ -1002,8 +1373,8 @@ export default { loadEquipmentStatusData() { if (this.equipmentStatusRequesting) return this.equipmentStatusRequesting = true - const data = this.CreateData('11', 'TV_设备实时状态_查询', []) - this.ExecDatabase(data) + const procName = 'TV_设备实时状态_查询' + this.execObservedDatabase('11', procName, [], { moduleName: '设备实时状态' }) .then((response) => { const list = response && response.data ? response.data : null if (this.isNoDataResult(list)) { @@ -1078,8 +1449,8 @@ export default { loadDeviceHistoryData() { if (this.deviceHistoryRequesting) return this.deviceHistoryRequesting = true - const data = this.CreateData('11', 'TV_设备运行履历_查询', []) - this.ExecDatabase(data) + const procName = 'TV_设备运行履历_查询' + this.execObservedDatabase('11', procName, [], { moduleName: '设备运行履历' }) .then((response) => { const list = response && response.data ? response.data : null if (this.isNoDataResult(list)) { @@ -1150,8 +1521,8 @@ export default { loadLineMonitorData() { if (this.lineMonitorRequesting) return this.lineMonitorRequesting = true - const data = this.CreateData('11', 'TV_线体任务联动状态_查询', []) - this.ExecDatabase(data) + const procName = 'TV_线体任务联动状态_查询' + this.execObservedDatabase('11', procName, [], { moduleName: '线体任务联动状态' }) .then((response) => { const list = response && response.data ? response.data : null if (this.isNoDataResult(list)) { @@ -1278,7 +1649,14 @@ export default { } }, changeView(value) { + const previousView = this.activeView this.activeView = value + if (previousView !== value) { + this.reportTelemetry('view_change', 'user_behavior', 'info', { + from_view: previousView, + to_view: value + }, { success: true }) + } if (this.dmtIframeReady) { this.syncActiveCamera() } @@ -1334,6 +1712,7 @@ export default { }, handleIframeLoad() { this.dmtIframeReady = false + this.reportIframeDomLoaded() if (this.iframeReadyTimer) { clearTimeout(this.iframeReadyTimer) this.iframeReadyTimer = null @@ -1355,6 +1734,7 @@ export default { clearTimeout(this.iframeReadyTimer) this.iframeReadyTimer = null } + this.reportIframeReady() this.syncActiveCamera() }, syncActiveCamera() { @@ -1396,15 +1776,23 @@ export default { if (this.modeSwitchTimer) { clearTimeout(this.modeSwitchTimer) } + this.startIframeTelemetry(mode) this.modeSwitchTimer = setTimeout(() => { this.mode = mode this.modeSwitchTimer = null }, 600) }, changeDtMode() { + this.reportTelemetry('mode_change', 'user_behavior', 'info', { + from_mode: this.currentMode, + to_mode: 'DT' + }, { success: true }) this.switchMode('DT') }, changeSmMode() { + this.reportTelemetry('simulation_rate_dialog_open', 'user_behavior', 'info', { + current_rate: this.selectedAutoPlayRate + }, { success: true }) this.openSimulationRateDialog() }, openSimulationRateDialog() { @@ -1413,14 +1801,27 @@ export default { }, selectSimulationRate(rate) { this.pendingAutoPlayRate = this.normalizeAutoPlayRate(rate) + this.reportTelemetry('simulation_rate_select', 'user_behavior', 'info', { + selected_rate: this.pendingAutoPlayRate + }, { success: true }) }, cancelSimulationRateDialog() { this.simulationRateDialogVisible = false + this.reportTelemetry('simulation_rate_dialog_cancel', 'user_behavior', 'info', { + current_rate: this.selectedAutoPlayRate + }, { success: true }) }, confirmSimulationRate() { this.selectedAutoPlayRate = this.normalizeAutoPlayRate(this.pendingAutoPlayRate) this.srcSm = this.buildIframeUrl(this.smIframeBase, this.autoPlayCraftCode, this.selectedAutoPlayRate) this.simulationRateDialogVisible = false + this.reportTelemetry('simulation_start', 'user_behavior', 'info', { + from_mode: this.currentMode, + to_mode: 'SM', + auto_play_rate: this.selectedAutoPlayRate, + craft_code: this.autoPlayCraftCode, + iframe_url: this.srcSm + }, { success: true }) this.switchMode('SM') }, disposeAlarmPieChart() {