🚀 智能上报方案
EzMonitor 采用智能化的数据上报策略,通过多种优化手段确保监控数据的高效、可靠传输。
📊 核心特性
1. 批量上报机制
通过批量聚合数据,减少网络请求次数,提升上报效率。
javascript
/**
* 延迟批量上报数据
*
* @param data 需要上报的数据
*/
export function lazyReportBatch(data: any) {
addCache(data);
const dataCache = getCache();
const reportData = async () => {
if (!dataCache.length) {
return;
}
sendServe(dataCache);
clearCache();
};
// 达到批量大小时立即上报
if (dataCache.length && dataCache.length > config.batchSize) {
reportData();
} else {
// 浏览器空闲时机上报
if ('requestIdleCallback' in window) {
window.requestIdleCallback(reportData, { timeout: 1000 });
} else {
setTimeout(reportData, 1000);
}
}
}优势:
- ✅ 减少 HTTP 请求次数
- ✅ 降低服务器压力
- ✅ 提升上报效率
2. 智能请求方式选择
根据数据大小和浏览器支持情况,自动选择最优的上报方式。
javascript
const sendServe = (reportData: any) => {
let sendType = 'xhr';
let sendTraceServer = xhrRequest;
const ObjectSize = isObjectSize(reportData); // 计算数据大小(KB)
// 1. 如果强制开启 Ajax
if (config.isAjax) {
sendTraceServer = xhrRequest;
sendType = 'xhr';
}
// 2. 如果支持 sendBeacon 且数据 < 60KB
else if (isSupportSendBeacon() && ObjectSize < 60) {
sendTraceServer = beaconRequest;
sendType = 'beacon';
}
// 3. 如果数据 < 2KB
else if (ObjectSize < 2) {
sendTraceServer = imgRequest;
sendType = 'img';
}
// 构造上报数据
reportData = {
data: reportData,
userId: config.userId,
sendType,
};
const jsonData = JSON.stringify(reportData);
// 执行上报
sendTraceServer(jsonData)
.then((res) => console.log('上报成功', res))
.catch((error) => console.error('上报失败', error));
};📋 三种上报方式对比
| 方式 | 适用场景 | 数据限制 | 可靠性 | 特点 |
|---|---|---|---|---|
| XHR | 默认方式 | 无限制 | ⭐⭐⭐ | 支持回调、错误处理 |
| sendBeacon | 小数据量 | < 64KB | ⭐⭐⭐⭐⭐ | 页面卸载时仍可发送 |
| Image | 极小数据 | < 2KB | ⭐⭐ | 兼容性最好 |
优势:
- ✅ 自动选择最优方式
- ✅ 兼容性好
- ✅ 提升上报成功率
3. 空闲时机上报
利用 requestIdleCallback API,在浏览器空闲时上报数据,避免阻塞用户操作。
javascript
// 浏览器空闲时机上报
if ('requestIdleCallback' in window) {
window.requestIdleCallback(reportData, { timeout: 1000 });
} else {
setTimeout(reportData, 1000);
}优势:
- ✅ 不阻塞主线程
- ✅ 不影响用户体验
- ✅ 自动降级兼容
4. 🔄 离线数据持久化与恢复(新增)
EzMonitor v1.1 新增了基于 LocalStorage 的数据持久化能力,支持离线数据暂存和恢复,确保在网络异常情况下不丢失监控数据。
💾 核心特性
4.1 自动持久化
每次添加数据到缓存时,自动保存到 LocalStorage,确保数据不丢失。
typescript
/**
* 缓存管理器
*/
class CacheManager {
addCache(data: any): void {
this.cache.push(data);
// 检查缓存大小限制
if (this.cache.length > this.config.maxCacheSize) {
console.warn(`[EzMonitor] 缓存超出最大限制,将清理旧数据`);
this.cache = this.cache.slice(-this.config.maxCacheSize);
}
// 自动持久化到 LocalStorage
if (this.config.enableLocalStorage) {
this.saveToLocalStorage();
}
}
}4.2 启动时自动恢复
页面加载时自动从 LocalStorage 恢复未上报的数据。
typescript
/**
* 从 LocalStorage 恢复数据
*/
private restoreFromLocalStorage(): void {
const stored = localStorage.getItem(this.config.localStorageKey);
if (!stored) return;
const persistentCache = JSON.parse(stored);
// 过滤过期数据
const now = Date.now();
const validItems = persistentCache.items.filter(
item => now - item.timestamp < this.config.cacheExpireTime
);
if (validItems.length > 0) {
this.cache = validItems.map(item => item.data);
console.log(`[EzMonitor] 从 LocalStorage 恢复 ${validItems.length} 条离线数据`);
}
}4.3 网络状态监听
自动监听网络状态变化,在网络恢复时立即上报离线数据。
typescript
/**
* 监听在线状态变化
*/
private setupOnlineListener(): void {
window.addEventListener('online', () => {
console.log('[EzMonitor] 网络已恢复,准备上报离线数据');
window.dispatchEvent(new CustomEvent('ez-monitor-online'));
});
window.addEventListener('offline', () => {
console.log('[EzMonitor] 网络已断开,数据将暂存到本地');
});
}4.4 自动上报离线数据
网络恢复时,自动上报 LocalStorage 中的离线数据。
typescript
/**
* 初始化上报系统
*/
export function initReportSystem() {
// 监听网络恢复事件
window.addEventListener('ez-monitor-online', () => {
const cacheManager = getCacheManager();
const offlineData = cacheManager.getCache();
if (offlineData.length > 0) {
console.log(`[EzMonitor] 网络已恢复,开始上报 ${offlineData.length} 条离线数据`);
// 批量上报离线数据
sendServe(offlineData);
cacheManager.clearCache();
}
});
// 页面加载时检查是否有离线数据
const cacheManager = getCacheManager();
const offlineData = cacheManager.getCache();
if (offlineData.length > 0 && navigator.onLine) {
console.log(`[EzMonitor] 检测到 ${offlineData.length} 条离线数据,开始上报`);
// 延迟上报,避免阻塞页面加载
setTimeout(() => {
sendServe(offlineData);
cacheManager.clearCache();
}, 1000);
}
}⚙️ 配置选项
typescript
interface CacheConfig {
enableLocalStorage?: boolean; // 是否启用 LocalStorage 持久化,默认 true
localStorageKey?: string; // LocalStorage 存储键名,默认 'ez_monitor_cache'
maxCacheSize?: number; // 最大缓存条数,默认 100
cacheExpireTime?: number; // 缓存过期时间(毫秒),默认 24小时
}🚀 使用示例
typescript
import EzMonitor from '@ezstars/monitor-sdk';
// 初始化时配置缓存策略
EzMonitor.init({
url: 'http://127.0.0.1:3001/monitor',
appId: 'your-app-id',
userId: 'user-123',
batchSize: 5,
// 缓存配置
enableLocalStorage: true, // 启用 LocalStorage 持久化
localStorageKey: 'my_monitor_cache', // 自定义存储键名
maxCacheSize: 100, // 最多缓存 100 条数据
cacheExpireTime: 24 * 60 * 60 * 1000, // 缓存 24 小时后过期
});📊 数据结构
typescript
// LocalStorage 存储结构
interface PersistentCache {
version: string; // 缓存版本号 '1.0.0'
items: CacheItem[]; // 缓存项数组
}
interface CacheItem {
data: any; // 监控数据
timestamp: number; // 数据添加时间戳
}🎯 离线场景示例
typescript
// 场景 1: 用户在地铁中打开应用(无网络)
// - 用户操作 → SDK 采集数据
// - 数据添加到缓存 → 自动保存到 LocalStorage
// - 控制台输出: "[EzMonitor] 网络已断开,数据将暂存到本地"
// 场景 2: 用户走出地铁(网络恢复)
// - 浏览器触发 'online' 事件
// - SDK 自动从 LocalStorage 读取离线数据
// - 控制台输出: "[EzMonitor] 网络已恢复,开始上报 15 条离线数据"
// - 批量上报成功 → 清空缓存和 LocalStorage
// 场景 3: 用户刷新页面
// - SDK 初始化时自动从 LocalStorage 恢复数据
// - 控制台输出: "[EzMonitor] 从 LocalStorage 恢复 8 条离线数据"
// - 检测到在线状态 → 延迟 1 秒后自动上报🔒 安全性保障
版本兼容性检查
typescriptif (persistentCache.version !== this.CACHE_VERSION) { console.warn('[EzMonitor] 缓存版本不匹配,清空旧缓存'); this.clearLocalStorage(); return; }过期数据自动清理
typescriptconst validItems = persistentCache.items.filter( item => now - item.timestamp < this.config.cacheExpireTime );LocalStorage 可用性检测
typescriptprivate isLocalStorageAvailable(): boolean { try { const testKey = '__ez_monitor_test__'; localStorage.setItem(testKey, 'test'); localStorage.removeItem(testKey); return true; } catch { return false; } }异常处理
typescripttry { localStorage.setItem(key, value); } catch (error) { console.warn('[EzMonitor] 保存到 LocalStorage 失败:', error); // 如果存储失败(可能是空间不足),尝试清理过期数据 this.clearExpiredCache(); }
📈 性能优化
页面隐藏时自动保存
typescriptdocument.addEventListener('visibilitychange', () => { if (document.hidden && this.config.enableLocalStorage) { this.saveToLocalStorage(); // 用户切换标签时保存数据 } });页面卸载前保存
typescriptwindow.addEventListener('beforeunload', () => { if (this.config.enableLocalStorage) { this.saveToLocalStorage(); // 页面关闭前保存数据 } });缓存大小限制
- 超过最大限制时,自动清理旧数据
- 避免 LocalStorage 占用过多空间
✅ 优势
| 特性 | 说明 | 收益 |
|---|---|---|
| 数据不丢失 | 离线时数据持久化到本地 | 确保监控数据完整性 |
| 自动恢复 | 页面刷新后自动恢复未上报数据 | 无需手动处理 |
| 智能上报 | 网络恢复时自动上报 | 减少数据延迟 |
| 容量控制 | 限制最大缓存条数 | 避免占用过多空间 |
| 过期清理 | 自动清理过期数据 | 保持存储空间健康 |
| 版本管理 | 缓存版本控制 | 避免数据结构不兼容 |
🎬 完整工作流程
用户操作/事件触发
↓
SDK 采集数据
↓
添加到内存缓存
↓
【自动保存到 LocalStorage】✅ 新增
↓
检查网络状态
↓
┌─────┴─────┐
在线 离线
↓ ↓
批量上报 【暂存本地】✅ 新增
↓ ↓
成功/失败 等待网络恢复
↓ ↓
清空缓存 【自动上报】✅ 新增
↓
清空缓存5. ⭐ SSE 实时推送(项目亮点)
采用 Server-Sent Events (SSE) 技术实现服务端主动推送,为监控系统提供实时数据流。
🎯 为什么使用 SSE?
相比传统的 HTTP 轮询和 WebSocket,SSE 更适合监控系统:
| 特性 | HTTP 轮询 | WebSocket | SSE |
|---|---|---|---|
| 通信方向 | 客户端拉取 | 双向 | 单向推送 ✅ |
| 协议 | HTTP | WS/WSS | HTTP/HTTPS ✅ |
| 实现复杂度 | 简单 | 复杂 | 简单 ✅ |
| 自动重连 | 需手动实现 | 需手动实现 | 浏览器自动 ✅ |
| 资源占用 | 高 | 中 | 低 ✅ |
| 适用场景 | 简单查询 | 聊天/游戏 | 监控/通知 ✅ |
📡 SSE 数据流架构
用户端 (浏览器)
↓
[SDK 上报数据]
↓
POST /monitor
↓
后端服务器
├─ 数据验证
├─ 分类处理 (performance/error/behavior/exception)
├─ 存储到内存
└─ 实时推送
↓
SSE 连接池
↓
[实时推送给所有订阅的客户端]
↓
监控面板 (Dashboard)
├─ 实时图表更新
├─ 性能指标展示
├─ 告警通知
└─ 统计数据刷新🔌 后端 SSE 实现
typescript
// SSE 连接管理
const sseClients = new Map<string, Map<string, PassThrough>>();
/**
* 建立 SSE 连接
*/
export const connectSSE = async (ctx: Context) => {
const { appId } = ctx.query;
// 设置 SSE 响应头
ctx.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// 创建流
const stream = new PassThrough();
ctx.body = stream;
// 生成客户端 ID
const clientId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// 存储客户端连接
if (!sseClients.has(appId as string)) {
sseClients.set(appId as string, new Map());
}
sseClients.get(appId as string)!.set(clientId, stream);
console.log(`✅ SSE 客户端连接: ${clientId}, appId: ${appId}`);
// 发送连接成功消息
stream.write(`event: connected\ndata: ${JSON.stringify({
clientId,
appId,
message: '连接成功',
timestamp: Date.now(),
})}\n\n`);
// 定期发送心跳,保持连接活跃
const heartbeat = setInterval(() => {
stream.write(`event: heartbeat\ndata: ${JSON.stringify({
timestamp: Date.now()
})}\n\n`);
}, 30000);
// 监听客户端断开连接
ctx.req.on('close', () => {
clearInterval(heartbeat);
sseClients.get(appId as string)?.delete(clientId);
console.log(`❌ SSE 客户端断开: ${clientId}`);
});
};
/**
* 向指定 appId 的所有客户端广播消息
*/
export function broadcastToApp(appId: string, event: string, data: any) {
const clients = sseClients.get(appId);
if (!clients || clients.size === 0) {
return;
}
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
clients.forEach((stream, clientId) => {
try {
stream.write(message);
} catch (error) {
console.error(`发送消息到客户端 ${clientId} 失败:`, error);
clients.delete(clientId);
}
});
}💻 前端 SSE 接收
typescript
// 建立 SSE 连接
const eventSource = new EventSource(
'http://127.0.0.1:3001/monitor/stream?appId=123456'
);
// 监听连接成功
eventSource.addEventListener('connected', (e) => {
const data = JSON.parse(e.data);
console.log('✅ SSE 连接成功:', data);
});
// 监听性能数据推送
eventSource.addEventListener('performance:fcp', (e) => {
const data = JSON.parse(e.data);
console.log('📊 收到 FCP 数据:', data);
updateFCPChart(data); // 更新图表
});
eventSource.addEventListener('performance:lcp', (e) => {
const data = JSON.parse(e.data);
console.log('📊 收到 LCP 数据:', data);
updateLCPChart(data);
});
// 监听慢请求告警
eventSource.addEventListener('performance:slow-request', (e) => {
const data = JSON.parse(e.data);
console.warn('⚠️ 慢请求告警:', data);
showAlert(`检测到慢请求: ${data.data.name} - ${data.duration}ms`);
});
// 监听错误告警
eventSource.addEventListener('error:alert', (e) => {
const data = JSON.parse(e.data);
console.error('🐛 错误告警:', data);
showErrorNotification(data);
});
// 监听实时统计
eventSource.addEventListener('performance:stats', (e) => {
const stats = JSON.parse(e.data);
console.log('📈 实时统计:', stats);
updateDashboard(stats); // 更新仪表盘
});
// 监听心跳
eventSource.addEventListener('heartbeat', (e) => {
console.log('💓 心跳:', e.data);
});
// 错误处理
eventSource.onerror = (error) => {
console.error('❌ SSE 连接错误:', error);
// EventSource 会自动重连
};
// 关闭连接
// eventSource.close();📊 SSE 事件类型
typescript
// 性能监控事件
'performance:fp' // First Paint
'performance:fcp' // First Contentful Paint
'performance:lcp' // Largest Contentful Paint
'performance:load' // 页面加载完成
'performance:fetch' // Fetch 请求
'performance:xhr' // XHR 请求
'performance:resource' // 资源加载
'performance:stats' // 实时统计数据
'performance:slow-request' // 慢请求告警
// 错误监控事件
'error:jsError' // JS 错误
'error:resourceError' // 资源加载错误
'error:promiseError' // Promise 错误
'error:alert' // 错误告警
// 行为监控事件
'behavior:pv' // 页面访问
'behavior:click' // 点击事件
'behavior:routerChange' // 路由变化
// 异常监控事件
'exception:whiteScreen' // 白屏
'exception:stutter' // 卡顿
'exception:crash' // 崩溃
'exception:critical' // 严重异常告警
// 系统事件
'connected' // 连接成功
'heartbeat' // 心跳🎨 实时监控大屏示例
typescript
import { useEffect, useState } from 'react';
function MonitorDashboard() {
const [performanceData, setPerformanceData] = useState([]);
const [stats, setStats] = useState({});
const [alerts, setAlerts] = useState([]);
useEffect(() => {
const eventSource = new EventSource(
'http://127.0.0.1:3001/monitor/stream?appId=123456'
);
// 实时接收性能数据
eventSource.addEventListener('performance:fcp', (e) => {
const data = JSON.parse(e.data);
setPerformanceData(prev => [data, ...prev].slice(0, 100));
});
// 实时接收统计数据
eventSource.addEventListener('performance:stats', (e) => {
const newStats = JSON.parse(e.data);
setStats(newStats);
});
// 实时接收告警
eventSource.addEventListener('performance:slow-request', (e) => {
const alert = JSON.parse(e.data);
setAlerts(prev => [alert, ...prev].slice(0, 10));
});
return () => eventSource.close();
}, []);
return (
<div className="dashboard">
<h1>实时监控大屏</h1>
{/* 实时统计 */}
<div className="stats">
<div>FCP: {stats.fcp?.avg}ms</div>
<div>LCP: {stats.lcp?.avg}ms</div>
<div>接口成功率: {stats.successRate}%</div>
</div>
{/* 实时数据流 */}
<div className="data-stream">
{performanceData.map(item => (
<div key={item.id}>{item.subType}: {item.duration}ms</div>
))}
</div>
{/* 实时告警 */}
<div className="alerts">
{alerts.map(alert => (
<div key={alert.timestamp} className="alert">
⚠️ {alert.message}
</div>
))}
</div>
</div>
);
}🎯 SSE 带来的优势
实时性强 ⚡
- 毫秒级延迟
- 数据产生后立即推送
- 无需前端轮询
资源占用低 💰
- 单个 HTTP 长连接
- 减少 99% 的无效请求
- 服务器压力小
实现简单 🛠️
- 基于标准 HTTP 协议
- 浏览器原生支持
- 无需额外的库
自动重连 🔄
- 连接断开自动重连
- 无需手动处理
- 保证数据可靠性
灵活推送 🎯
- 支持多种事件类型
- 按需订阅
- 分类推送
实时告警 🚨
- 慢请求立即通知
- 错误实时告警
- 异常及时响应
📈 性能对比
| 方案 | 请求次数/分钟 | 平均延迟 | 服务器压力 | 实时性 |
|---|---|---|---|---|
| 轮询 (3秒) | 20次 | 1.5秒 | 高 | 差 |
| 长轮询 | 10次 | 1秒 | 中 | 中 |
| WebSocket | 1个连接 | <100ms | 中 | 好 |
| SSE ✅ | 1个连接 | <50ms | 低 | 优秀 |
🔒 安全性考虑
typescript
// 1. 连接鉴权
export const connectSSE = async (ctx: Context) => {
const { appId, token } = ctx.query;
// 验证 token
if (!verifyToken(token)) {
ctx.status = 401;
ctx.body = { error: '未授权' };
return;
}
// ...建立连接
};
// 2. 限流控制
const connectionLimit = new Map<string, number>();
export const connectSSE = async (ctx: Context) => {
const appId = ctx.query.appId as string;
const currentConnections = connectionLimit.get(appId) || 0;
if (currentConnections >= 100) {
ctx.status = 429;
ctx.body = { error: '连接数超限' };
return;
}
connectionLimit.set(appId, currentConnections + 1);
// ...建立连接
};
// 3. 数据过滤
export function broadcastToApp(appId: string, event: string, data: any) {
// 过滤敏感信息
const safeData = sanitizeData(data);
// 发送给客户端
// ...
}🎯 上报策略总结
EzMonitor 的智能上报方案结合了:
- 批量聚合 - 减少请求次数
- 智能选择 - 根据数据大小选择最优方式
- 空闲上报 - 不阻塞用户操作
- 离线持久化 - LocalStorage 本地暂存,确保数据不丢失 ✨ 新增
- 自动恢复 - 网络恢复时自动上报离线数据 ✨ 新增
- SSE 推送 - 实时数据流,毫秒级延迟
- 自动重连 - 保证数据可靠性
- 多端同步 - 多个监控面板实时同步
这些优化使得 EzMonitor 能够在保证监控数据完整性的同时,最小化对用户体验和服务器性能的影响。
🚀 快速开始
SDK 配置
typescript
import EzMonitor from '@ezstars/monitor-sdk';
EzMonitor.init({
url: 'http://127.0.0.1:3001/monitor', // 上报地址
appId: 'your-app-id', // 应用 ID
userId: 'user-123', // 用户 ID
batchSize: 5, // 批量大小
isAjax: false, // 智能选择上报方式
// 缓存配置(新增)
enableLocalStorage: true, // 启用 LocalStorage 持久化
localStorageKey: 'ez_monitor_cache', // LocalStorage 键名
maxCacheSize: 100, // 最大缓存条数
cacheExpireTime: 24 * 60 * 60 * 1000, // 缓存过期时间(24小时)
});监控面板接入
typescript
// 建立 SSE 连接
const eventSource = new EventSource(
'http://127.0.0.1:3001/monitor/stream?appId=your-app-id'
);
// 订阅感兴趣的事件
eventSource.addEventListener('performance:fcp', handleFCP);
eventSource.addEventListener('performance:stats', handleStats);
eventSource.addEventListener('error:alert', handleAlert);API 查询
typescript
// 查询历史数据
fetch('http://127.0.0.1:3001/monitor/performance/list?appId=your-app-id')
.then(res => res.json())
.then(data => console.log(data));
// 查询统计信息
fetch('http://127.0.0.1:3001/monitor/performance/stats?appId=your-app-id')
.then(res => res.json())
.then(stats => console.log(stats));