Appearance
监控上报方案
概述
前端监控上报是指收集用户行为、性能数据、错误信息等,并上报到服务器进行分析,帮助开发者了解应用运行状态和用户体验。
监控类型
1. 性能监控
javascript
class PerformanceMonitor {
constructor() {
this.data = {}
}
// 页面加载性能
getPageLoadTiming() {
const timing = performance.timing
return {
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
request: timing.responseStart - timing.requestStart,
response: timing.responseEnd - timing.responseStart,
dom: timing.domComplete - timing.domInteractive,
load: timing.loadEventEnd - timing.navigationStart,
total: timing.loadEventEnd - timing.navigationStart
}
}
// 资源加载性能
getResourceTiming() {
const resources = performance.getEntriesByType('resource')
return resources.map(resource => ({
name: resource.name,
type: resource.initiatorType,
duration: resource.duration,
size: resource.transferSize
}))
}
// Web Vitals
async getWebVitals() {
const vitals = {}
// LCP (Largest Contentful Paint)
const lcpEntries = performance.getEntriesByType('largest-contentful-paint')
if (lcpEntries.length > 0) {
vitals.lcp = lcpEntries[lcpEntries.length - 1].renderTime
}
// FID (First Input Delay)
const fidEntries = performance.getEntriesByType('first-input')
if (fidEntries.length > 0) {
vitals.fid = fidEntries[0].processingStart - fidEntries[0].startTime
}
// CLS (Cumulative Layout Shift)
let clsValue = 0
const clsEntries = performance.getEntriesByType('layout-shift')
clsEntries.forEach(entry => {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
})
vitals.cls = clsValue
return vitals
}
// 收集所有性能数据
async collect() {
return {
pageLoad: this.getPageLoadTiming(),
resources: this.getResourceTiming(),
webVitals: await this.getWebVitals(),
timestamp: Date.now()
}
}
}2. 错误监控
javascript
class ErrorMonitor {
constructor() {
this.errors = []
}
// 监听 JavaScript 错误
listenJSErrors() {
window.onerror = (message, source, lineno, colno, error) => {
this.report({
type: 'js-error',
message,
source,
lineno,
colno,
stack: error?.stack,
timestamp: Date.now()
})
}
}
// 监听 Promise 错误
listenPromiseErrors() {
window.addEventListener('unhandledrejection', (event) => {
this.report({
type: 'promise-error',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: Date.now()
})
})
}
// 监听资源加载错误
listenResourceErrors() {
window.addEventListener('error', (event) => {
if (event.target !== window) {
this.report({
type: 'resource-error',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
timestamp: Date.now()
})
}
}, true)
}
// 监听 Vue 错误
listenVueErrors(Vue) {
Vue.config.errorHandler = (error, vm, info) => {
this.report({
type: 'vue-error',
message: error.message,
stack: error.stack,
component: vm?.$options?.name,
info,
timestamp: Date.now()
})
}
}
// 监听 React 错误
listenReactErrors() {
const originalConsoleError = console.error
console.error = (...args) => {
this.report({
type: 'react-error',
message: args.join(' '),
timestamp: Date.now()
})
originalConsoleError.apply(console, args)
}
}
// 上报错误
report(error) {
this.errors.push(error)
this.send(error)
}
// 发送到服务器
async send(error) {
try {
await fetch('/api/monitor/error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(error)
})
} catch (e) {
console.error('上报错误失败:', e)
}
}
// 初始化
init(options = {}) {
this.listenJSErrors()
this.listenPromiseErrors()
this.listenResourceErrors()
if (options.Vue) {
this.listenVueErrors(options.Vue)
}
if (options.React) {
this.listenReactErrors()
}
}
}3. 行为监控
javascript
class BehaviorMonitor {
constructor() {
this.events = []
}
// 监听点击事件
trackClick() {
document.addEventListener('click', (e) => {
this.record({
type: 'click',
target: e.target.tagName,
text: e.target.innerText?.slice(0, 50),
xpath: this.getXPath(e.target),
timestamp: Date.now()
})
}, true)
}
// 监听页面访问
trackPageView() {
// 初始页面
this.record({
type: 'page-view',
url: window.location.href,
title: document.title,
timestamp: Date.now()
})
// 监听路由变化
const originalPushState = history.pushState
history.pushState = function(...args) {
originalPushState.apply(history, args)
this.record({
type: 'page-view',
url: window.location.href,
title: document.title,
timestamp: Date.now()
})
}
window.addEventListener('popstate', () => {
this.record({
type: 'page-view',
url: window.location.href,
title: document.title,
timestamp: Date.now()
})
})
}
// 监听用户停留时间
trackStayTime() {
let startTime = Date.now()
window.addEventListener('beforeunload', () => {
const stayTime = Date.now() - startTime
this.record({
type: 'stay-time',
url: window.location.href,
duration: stayTime,
timestamp: Date.now()
})
})
}
// 获取 XPath
getXPath(element) {
if (element.id) {
return `//*[@id="${element.id}"]`
}
const parts = []
let current = element
while (current && current.nodeType === Node.ELEMENT_NODE) {
let index = 0
let sibling = current.previousSibling
while (sibling) {
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
index++
}
sibling = sibling.previousSibling
}
const tagName = current.tagName.toLowerCase()
const indexStr = index ? `[${index + 1}]` : ''
parts.unshift(`${tagName}${indexStr}`)
current = current.parentNode
}
return '/' + parts.join('/')
}
// 记录事件
record(event) {
this.events.push(event)
this.send(event)
}
// 发送到服务器
async send(event) {
try {
await fetch('/api/monitor/behavior', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
})
} catch (e) {
console.error('上报行为失败:', e)
}
}
// 初始化
init() {
this.trackClick()
this.trackPageView()
this.trackStayTime()
}
}数据上报
1. 上报方式
javascript
class Reporter {
constructor(options = {}) {
this.url = options.url || '/api/monitor'
this.batchSize = options.batchSize || 10
this.batchInterval = options.batchInterval || 5000
this.queue = []
this.timer = null
}
// 添加到队列
add(data) {
this.queue.push(data)
if (this.queue.length >= this.batchSize) {
this.flush()
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.batchInterval)
}
}
// 批量发送
async flush() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
if (this.queue.length === 0) return
const data = this.queue.slice()
this.queue = []
try {
// 使用 sendBeacon 优先
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' })
navigator.sendBeacon(this.url, blob)
} else {
await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
keepalive: true
})
}
} catch (e) {
console.error('上报失败:', e)
// 失败时重新加入队列
this.queue.unshift(...data)
}
}
}2. 数据格式
javascript
{
// 应用信息
appId: 'your-app-id',
version: '1.0.0',
// 用户信息
userId: 'user-123',
deviceId: 'device-456',
// 环境信息
url: 'https://example.com/page',
userAgent: 'Mozilla/5.0...',
screen: '1920x1080',
viewport: '1200x800',
// 监控数据
type: 'performance', // performance, error, behavior
data: {
// 具体数据
},
// 时间戳
timestamp: 1234567890000
}最佳实践
1. 数据采样
- 根据流量设置采样率
- 重要错误 100% 上报
- 性能数据采样上报
2. 性能优化
- 使用 sendBeacon
- 批量上报
- 数据压缩
3. 隐私保护
- 脱敏敏感信息
- 遵守隐私法规
- 用户授权