一、技术实现方案
1.1 监控平台架构
监控平台架构
├── 前端SDK层
│ ├── 数据采集模块
│ ├── 性能监控模块
│ ├── 错误监控模块
│ └── 埋点追踪模块
│
├── 数据上报层
│ ├── 上报策略
│ ├── 数据压缩
│ ├── 离线缓存
│ └── 请求队列
│
├── 服务端接收层
│ ├── 数据接收API
│ ├── 数据验证
│ ├── 数据解析
│ └── 数据清洗
│
├── 数据存储层
│ ├── MongoDB (原始数据)
│ ├── Redis (实时计算)
│ ├── ClickHouse (分析查询)
│ └── ElasticSearch (日志检索)
│
├── 数据分析层
│ ├── 实时计算
│ ├── 离线分析
│ ├── 数据聚合
│ └── 报表生成
│
└── 可视化展示层
├── 实时监控大屏
├── 性能分析报表
├── 错误统计图表
└── 告警通知
1.2 技术栈
前端SDK
- 构建工具: Rollup
- 压缩库: pako (gzip)
- 工具库: axios
服务端
- Node.js + Express
- 数据库: MongoDB, Redis, ClickHouse
- 消息队列: RabbitMQ / Kafka
可视化
- Vue 3 + ECharts
- WebSocket (实时推送)
二、数据采集SDK (基于Rollup打包)
2.1 SDK项目结构
monitor-sdk/
├── src/
│ ├── core/
│ │ ├── base.js # 基础类
│ │ ├── config.js # 配置管理
│ │ └── report.js # 上报模块
│ ├── modules/
│ │ ├── performance.js # 性能监控
│ │ ├── error.js # 错误监控
│ │ ├── behavior.js # 行为埋点
│ │ └── exposure.js # 曝光监控
│ ├── utils/
│ │ ├── compress.js # 数据压缩
│ │ ├── cache.js # 缓存管理
│ │ └── helpers.js # 工具函数
│ └── index.js # 入口文件
├── rollup.config.js # Rollup配置
├── package.json
└── README.md
2.2 Rollup配置文件
rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import terser from '@rollup/plugin-terser'
import json from '@rollup/plugin-json'
export default {
input: 'src/index.js',
output: [
{
file: 'dist/monitor-sdk.js',
format: 'umd',
name: 'MonitorSDK',
sourcemap: true
},
{
file: 'dist/monitor-sdk.min.js',
format: 'umd',
name: 'MonitorSDK',
sourcemap: true,
plugins: [terser()]
},
{
file: 'dist/monitor-sdk.esm.js',
format: 'es',
sourcemap: true
},
{
file: 'dist/monitor-sdk.cjs.js',
format: 'cjs',
sourcemap: true
}
],
plugins: [
resolve(),
commonjs(),
json(),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
presets: [
['@babel/preset-env', {
targets: {
browsers: ['> 1%', 'last 2 versions', 'not dead']
}
}]
]
})
]
}
2.3 SDK核心代码
src/core/base.js
export class MonitorBase {
constructor() {
this.config = null
this.modules = {}
}
// 初始化
init(config) {
this.config = {
appId: config.appId,
apiUrl: config.apiUrl,
enablePerformance: config.enablePerformance !== false,
enableError: config.enableError !== false,
enableBehavior: config.enableBehavior !== false,
enableExposure: config.enableExposure !== false,
reportInterval: config.reportInterval || 5000,
maxQueueSize: config.maxQueueSize || 10,
sampleRate: config.sampleRate || 1,
userId: config.userId,
debug: config.debug || false
}
// 采样判断
if (Math.random() > this.config.sampleRate) {
console.log('Monitor SDK: Sampling skipped')
return
}
this.initModules()
}
// 初始化模块
initModules() {
const modules = []
if (this.config.enablePerformance) {
modules.push('performance')
}
if (this.config.enableError) {
modules.push('error')
}
if (this.config.enableBehavior) {
modules.push('behavior')
}
if (this.config.enableExposure) {
modules.push('exposure')
}
modules.forEach(moduleName => {
this.loadModule(moduleName)
})
}
// 加载模块
loadModule(moduleName) {
try {
const Module = require(`../modules/${moduleName}`).default
this.modules[moduleName] = new Module(this.config)
this.modules[moduleName].init()
} catch (error) {
console.error(`Failed to load module: ${moduleName}`, error)
}
}
// 获取模块
getModule(moduleName) {
return this.modules[moduleName]
}
// 销毁
destroy() {
Object.values(this.modules).forEach(module => {
if (module.destroy) {
module.destroy()
}
})
this.modules = {}
}
}
src/core/report.js
import { compress } from '../utils/compress'
import { CacheManager } from '../utils/cache'
export class ReportManager {
constructor(config) {
this.config = config
this.queue = []
this.cacheManager = new CacheManager()
this.timer = null
this.init()
}
init() {
// 启动定时上报
this.startScheduleReport()
// 页面卸载时上报
this.setupUnloadReport()
// 恢复缓存数据
this.recoverCache()
}
// 添加到队列
add(data) {
this.queue.push({
...data,
appId: this.config.appId,
userId: this.config.userId,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
})
if (this.queue.length >= this.config.maxQueueSize) {
this.report()
}
}
// 上报数据
async report() {
if (this.queue.length === 0) return
const data = [...this.queue]
this.queue = []
try {
// 压缩数据
const compressed = compress(data)
// 发送数据
await this.send(compressed)
if (this.config.debug) {
console.log('Report success:', data)
}
} catch (error) {
console.error('Report failed:', error)
// 保存到缓存
this.cacheManager.save(data)
}
}
// 发送数据
async send(data) {
// 优先使用Beacon API
if (navigator.sendBeacon) {
const blob = new Blob([data], { type: 'application/json' })
const success = navigator.sendBeacon(this.config.apiUrl, blob)
if (success) return
}
// 降级到fetch
const response = await fetch(this.config.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip'
},
body: data,
keepalive: true
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
}
// 定时上报
startScheduleReport() {
this.timer = setInterval(() => {
this.report()
}, this.config.reportInterval)
}
// 页面卸载上报
setupUnloadReport() {
window.addEventListener('beforeunload', () => {
this.report()
})
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.report()
}
})
}
// 恢复缓存
recoverCache() {
const cached = this.cacheManager.load()
if (cached && cached.length > 0) {
this.queue.push(...cached)
this.cacheManager.clear()
}
}
// 销毁
destroy() {
if (this.timer) {
clearInterval(this.timer)
}
this.report()
}
}
src/utils/compress.js
import pako from 'pako'
// 压缩数据
export function compress(data) {
try {
const json = JSON.stringify(data)
const compressed = pako.gzip(json)
return compressed
} catch (error) {
console.error('Compress failed:', error)
return JSON.stringify(data)
}
}
// 解压数据
export function decompress(data) {
try {
const decompressed = pako.ungzip(data, { to: 'string' })
return JSON.parse(decompressed)
} catch (error) {
console.error('Decompress failed:', error)
return null
}
}
src/utils/cache.js
export class CacheManager {
constructor() {
this.key = 'monitor_cache'
this.maxSize = 100
}
// 保存
save(data) {
try {
const cached = this.load() || []
cached.push(...data)
// 限制数量
const limited = cached.slice(-this.maxSize)
localStorage.setItem(this.key, JSON.stringify(limited))
} catch (error) {
console.error('Cache save failed:', error)
}
}
// 加载
load() {
try {
const cached = localStorage.getItem(this.key)
return cached ? JSON.parse(cached) : []
} catch (error) {
console.error('Cache load failed:', error)
return []
}
}
// 清空
clear() {
try {
localStorage.removeItem(this.key)
} catch (error) {
console.error('Cache clear failed:', error)
}
}
}
src/index.js
import { MonitorBase } from './core/base'
import { ReportManager } from './core/report'
class MonitorSDK extends MonitorBase {
constructor() {
super()
this.reportManager = null
}
init(config) {
super.init(config)
// 初始化上报管理器
this.reportManager = new ReportManager(this.config)
}
// 手动上报
track(eventName, eventData) {
this.reportManager.add({
type: 'track',
eventName,
eventData
})
}
// 设置用户ID
setUserId(userId) {
this.config.userId = userId
}
// 获取版本
getVersion() {
return '1.0.0'
}
}
// 创建全局实例
const monitor = new MonitorSDK()
// 导出
export default monitor
// 支持script标签引入
if (typeof window !== 'undefined') {
window.MonitorSDK = monitor
}
package.json
{
"name": "monitor-sdk",
"version": "1.0.0",
"description": "前端监控SDK",
"main": "dist/monitor-sdk.cjs.js",
"module": "dist/monitor-sdk.esm.js",
"browser": "dist/monitor-sdk.min.js",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"test": "jest"
},
"keywords": ["monitor", "performance", "error", "tracking"],
"author": "",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.23.0",
"@babel/preset-env": "^7.23.0",
"@rollup/plugin-babel": "^6.0.0",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-terser": "^0.4.0",
"rollup": "^4.0.0"
},
"dependencies": {
"pako": "^2.1.0"
}
}
三、数据上报策略
3.1 智能上报策略
smart-report-strategy.js
export class SmartReportStrategy {
constructor(config) {
this.config = config
this.queue = {
high: [], // 高优先级:立即上报
normal: [], // 普通优先级:批量上报
low: [] // 低优先级:延迟上报
}
this.init()
}
init() {
// 监听网络状态
this.monitorNetwork()
// 根据优先级设置不同的上报策略
this.setupReportStrategies()
}
// 添加数据
add(data, priority = 'normal') {
this.queue[priority].push({
data,
timestamp: Date.now()
})
this.checkReport(priority)
}
// 检查是否需要上报
checkReport(priority) {
const strategies = {
high: () => {
// 高优先级立即上报
if (this.queue.high.length > 0) {
this.report('high')
}
},
normal: () => {
// 普通优先级:10条或5秒
if (this.queue.normal.length >= 10) {
this.report('normal')
}
},
low: () => {
// 低优先级:50条或30秒
if (this.queue.low.length >= 50) {
this.report('low')
}
}
}
strategies[priority]?.()
}
// 设置上报策略
setupReportStrategies() {
// 普通优先级:5秒一次
setInterval(() => {
if (this.queue.normal.length > 0) {
this.report('normal')
}
}, 5000)
// 低优先级:30秒一次
setInterval(() => {
if (this.queue.low.length > 0) {
this.report('low')
}
}, 30000)
}
// 上报数据
async report(priority) {
const items = [...this.queue[priority]]
this.queue[priority] = []
if (items.length === 0) return
try {
const data = items.map(item => item.data)
await this.send(data, priority)
} catch (error) {
console.error('Report failed:', error)
// 失败的数据降级处理
if (priority === 'high') {
// 高优先级失败,降级到普通
this.queue.normal.push(...items)
} else {
// 其他优先级失败,缓存到localStorage
this.saveToCache(items.map(item => item.data))
}
}
}
// 发送数据
async send(data, priority) {
const payload = {
priority,
data,
timestamp: Date.now()
}
// 根据网络状况选择上报方式
if (this.isSlowNetwork()) {
// 慢速网络,只上报关键数据
if (priority !== 'high') {
throw new Error('Network too slow, skip non-high priority')
}
}
const response = await fetch(this.config.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
}
// 监控网络状态
monitorNetwork() {
if ('connection' in navigator) {
this.networkInfo = navigator.connection
navigator.connection.addEventListener('change', () => {
console.log('Network changed:', {
effectiveType: this.networkInfo.effectiveType,
downlink: this.networkInfo.downlink,
rtt: this.networkInfo.rtt
})
})
}
}
// 判断是否为慢速网络
isSlowNetwork() {
if (!this.networkInfo) return false
const effectiveType = this.networkInfo.effectiveType
return effectiveType === 'slow-2g' || effectiveType === '2g'
}
// 保存到缓存
saveToCache(data) {
try {
const cached = JSON.parse(localStorage.getItem('report_cache') || '[]')
cached.push(...data)
localStorage.setItem('report_cache', JSON.stringify(cached.slice(-100)))
} catch (error) {
console.error('Save to cache failed:', error)
}
}
}
四、数据清洗与存储
4.1 服务端数据处理
server/data-processor.js
class DataProcessor {
// 数据验证
validate(data) {
if (!Array.isArray(data)) {
return { valid: false, message: 'Data must be array' }
}
for (const item of data) {
if (!item.appId) {
return { valid: false, message: 'appId is required' }
}
if (!item.timestamp) {
return { valid: false, message: 'timestamp is required' }
}
}
return { valid: true }
}
// 数据清洗
clean(data) {
return data.map(item => {
const cleaned = { ...item }
// URL脱敏
if (cleaned.url) {
cleaned.url = this.desensitizeUrl(cleaned.url)
}
// 标准化时间
cleaned.timestamp = new Date(cleaned.timestamp).toISOString()
// 提取设备信息
if (cleaned.userAgent) {
cleaned.browser = this.extractBrowser(cleaned.userAgent)
cleaned.os = this.extractOS(cleaned.userAgent)
cleaned.device = this.extractDevice(cleaned.userAgent)
}
return cleaned
})
}
// URL脱敏
desensitizeUrl(url) {
try {
const urlObj = new URL(url)
const sensitiveParams = ['token', 'password', 'key', 'secret']
sensitiveParams.forEach(param => {
urlObj.searchParams.delete(param)
})
return urlObj.toString()
} catch (error) {
return url
}
}
// 提取浏览器
extractBrowser(ua) {
if (/Chrome/i.test(ua)) return 'Chrome'
if (/Safari/i.test(ua)) return 'Safari'
if (/Firefox/i.test(ua)) return 'Firefox'
if (/Edge/i.test(ua)) return 'Edge'
return 'Unknown'
}
// 提取操作系统
extractOS(ua) {
if (/Windows/i.test(ua)) return 'Windows'
if (/Mac/i.test(ua)) return 'MacOS'
if (/Linux/i.test(ua)) return 'Linux'
if (/Android/i.test(ua)) return 'Android'
if (/iOS/i.test(ua)) return 'iOS'
return 'Unknown'
}
// 提取设备
extractDevice(ua) {
if (/Mobile/i.test(ua)) return 'Mobile'
if (/Tablet/i.test(ua)) return 'Tablet'
return 'Desktop'
}
// 数据分类
categorize(data) {
const categorized = {
performance: [],
error: [],
behavior: [],
exposure: []
}
data.forEach(item => {
if (item.type === 'performance') {
categorized.performance.push(item)
} else if (item.type === 'error') {
categorized.error.push(item)
} else if (item.type === 'click' || item.type === 'track') {
categorized.behavior.push(item)
} else if (item.type === 'exposure') {
categorized.exposure.push(item)
}
})
return categorized
}
}
module.exports = new DataProcessor()
五、可视化报表
5.1 监控大屏组件
MonitorDashboard.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const statistics = ref({
pv: 0,
uv: 0,
errorCount: 0,
avgLoadTime: 0
})
const performanceChart = ref(null)
const errorChart = ref(null)
// 初始化图表
const initCharts = () => {
initPerformanceChart()
initErrorChart()
}
// 性能趋势图
const initPerformanceChart = () => {
const chart = echarts.init(performanceChart.value)
const option = {
title: {
text: '页面加载性能趋势',
left: 'center'
},
tooltip: { trigger: 'axis' },
legend: {
data: ['FCP', 'LCP', 'TTFB'],
bottom: 0
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value',
name: '时间 (ms)'
},
series: [
{
name: 'FCP',
type: 'line',
data: [],
itemStyle: { color: '#409eff' }
},
{
name: 'LCP',
type: 'line',
data: [],
itemStyle: { color: '#67c23a' }
},
{
name: 'TTFB',
type: 'line',
data: [],
itemStyle: { color: '#e6a23c' }
}
]
}
chart.setOption(option)
}
// 错误分布图
const initErrorChart = () => {
const chart = echarts.init(errorChart.value)
const option = {
title: {
text: '错误类型分布',
left: 'center'
},
tooltip: {
trigger: 'item'
},
series: [
{
type: 'pie',
radius: '60%',
data: []
}
]
}
chart.setOption(option)
}
onMounted(() => {
initCharts()
})
</script>
<template>
<div class="monitor-dashboard">
<div class="dashboard-header">
<h1>监控大屏</h1>
</div>
<!-- 统计卡片 -->
<div class="statistics-grid">
<div class="stat-card">
<div class="stat-label">今日PV</div>
<div class="stat-value">{{ statistics.pv.toLocaleString() }}</div>
</div>
<div class="stat-card">
<div class="stat-label">今日UV</div>
<div class="stat-value">{{ statistics.uv.toLocaleString() }}</div>
</div>
<div class="stat-card">
<div class="stat-label">错误数</div>
<div class="stat-value">{{ statistics.errorCount }}</div>
</div>
<div class="stat-card">
<div class="stat-label">平均加载</div>
<div class="stat-value">{{ statistics.avgLoadTime }}ms</div>
</div>
</div>
<!-- 图表 -->
<div class="charts-grid">
<div class="chart-card">
<div ref="performanceChart" class="chart"></div>
</div>
<div class="chart-card">
<div ref="errorChart" class="chart"></div>
</div>
</div>
</div>
</template>
<style scoped>
.monitor-dashboard {
padding: 20px;
background: #0a0e27;
min-height: 100vh;
color: white;
}
.dashboard-header h1 {
font-size: 32px;
text-align: center;
margin-bottom: 30px;
}
.statistics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
padding: 30px;
background: rgba(64, 158, 255, 0.1);
border: 1px solid rgba(64, 158, 255, 0.2);
border-radius: 12px;
text-align: center;
}
.stat-label {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12px;
}
.stat-value {
font-size: 36px;
font-weight: 700;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.chart-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 20px;
}
.chart {
width: 100%;
height: 400px;
}
</style>
六、告警机制
6.1 告警规则引擎
alert-engine.js
export class AlertEngine {
constructor() {
this.rules = []
this.initDefaultRules()
}
// 初始化规则
initDefaultRules() {
this.addRule({
id: 'high_error_rate',
name: '错误率过高',
condition: (data) => {
return (data.errorCount / data.totalRequest) > 0.05
},
level: 'critical',
message: (data) => `错误率${(data.errorCount / data.totalRequest * 100).toFixed(2)}%`,
channels: ['dingtalk', 'email']
})
this.addRule({
id: 'slow_page_load',
name: '页面加载慢',
condition: (data) => {
return data.avgLoadTime > 3000
},
level: 'warning',
message: (data) => `平均加载${data.avgLoadTime}ms`,
channels: ['dingtalk']
})
}
// 添加规则
addRule(rule) {
this.rules.push(rule)
}
// 检查告警
async checkAlerts(data) {
const alerts = []
for (const rule of this.rules) {
if (rule.condition(data)) {
const alert = {
id: Date.now(),
ruleId: rule.id,
ruleName: rule.name,
level: rule.level,
message: rule.message(data),
timestamp: Date.now()
}
alerts.push(alert)
await this.sendAlert(alert, rule.channels)
}
}
return alerts
}
// 发送告警
async sendAlert(alert, channels) {
for (const channel of channels) {
if (channel === 'dingtalk') {
await this.sendToDingTalk(alert)
} else if (channel === 'email') {
await this.sendToEmail(alert)
}
}
}
// 发送钉钉
async sendToDingTalk(alert) {
const webhook = process.env.DINGTALK_WEBHOOK
const content = {
msgtype: 'markdown',
markdown: {
title: alert.ruleName,
text: `### ${alert.ruleName}\n**级别**: ${alert.level}\n**消息**: ${alert.message}`
}
}
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(content)
})
}
// 发送邮件
async sendToEmail(alert) {
console.log('Send email:', alert)
}
}
七、简历描述模板
监控平台架构师 (2024.06 - 至今)
负责设计和搭建公司级前端监控平台,覆盖20+业务系统,日均处理监控数据100万+条。
核心职责
- 基于Rollup构建监控SDK,实现全链路监控
- 设计智能上报策略,数据丢失率<0.1%
- 搭建数据处理系统,实现清洗和存储
- 开发可视化大屏,实时展示监控数据
- 建立告警系统,支持多渠道通知
技术实现
- SDK采用模块化设计,压缩后15KB
- 使用pako压缩,节省30%带宽
- 数据存储采用MongoDB+Redis架构
- WebSocket实时推送
- 告警引擎支持规则配置
项目成果
- SDK覆盖率100%
- 日均处理100万数据
- 错误响应时间从2小时到15分钟
- 故障率下降60%
八、SOP标准回答
面试问题: 如何搭建监控平台?
标准回答
"监控平台分五层。第一层SDK用Rollup打包,支持tree-shaking。第二层上报有三种优先级策略。第三层服务端做数据清洗和验证。第四层数据存储用MongoDB原始数据、Redis实时计算。第五层可视化用ECharts展示。告警引擎支持规则配置和多渠道通知。"
九、部署配置
docker-compose.yml
version: '3.8'
services:
monitor-api:
build: ./server
ports:
- "3000:3000"
environment:
- MONGODB_URI=mongodb://mongo:27017/monitor
- REDIS_URI=redis://redis:6379
depends_on:
- mongo
- redis
mongo:
image: mongo:6.0
volumes:
- mongo-data:/data/db
redis:
image: redis:7.0
volumes:
- redis-data:/data
volumes:
mongo-data:
redis-data: