feat: add qingan dashboard telemetry

This commit is contained in:
Cui Hongwei
2026-07-28 09:32:25 +08:00
parent a43a13b178
commit 679c6c0af5

View File

@@ -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() {