Skip to content

🚀 智能上报方案

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 秒后自动上报

🔒 安全性保障

  1. 版本兼容性检查

    typescript
    if (persistentCache.version !== this.CACHE_VERSION) {
      console.warn('[EzMonitor] 缓存版本不匹配,清空旧缓存');
      this.clearLocalStorage();
      return;
    }
  2. 过期数据自动清理

    typescript
    const validItems = persistentCache.items.filter(
      item => now - item.timestamp < this.config.cacheExpireTime
    );
  3. LocalStorage 可用性检测

    typescript
    private isLocalStorageAvailable(): boolean {
      try {
        const testKey = '__ez_monitor_test__';
        localStorage.setItem(testKey, 'test');
        localStorage.removeItem(testKey);
        return true;
      } catch {
        return false;
      }
    }
  4. 异常处理

    typescript
    try {
      localStorage.setItem(key, value);
    } catch (error) {
      console.warn('[EzMonitor] 保存到 LocalStorage 失败:', error);
      // 如果存储失败(可能是空间不足),尝试清理过期数据
      this.clearExpiredCache();
    }

📈 性能优化

  1. 页面隐藏时自动保存

    typescript
    document.addEventListener('visibilitychange', () => {
      if (document.hidden && this.config.enableLocalStorage) {
        this.saveToLocalStorage(); // 用户切换标签时保存数据
      }
    });
  2. 页面卸载前保存

    typescript
    window.addEventListener('beforeunload', () => {
      if (this.config.enableLocalStorage) {
        this.saveToLocalStorage(); // 页面关闭前保存数据
      }
    });
  3. 缓存大小限制

    • 超过最大限制时,自动清理旧数据
    • 避免 LocalStorage 占用过多空间

✅ 优势

特性说明收益
数据不丢失离线时数据持久化到本地确保监控数据完整性
自动恢复页面刷新后自动恢复未上报数据无需手动处理
智能上报网络恢复时自动上报减少数据延迟
容量控制限制最大缓存条数避免占用过多空间
过期清理自动清理过期数据保持存储空间健康
版本管理缓存版本控制避免数据结构不兼容

🎬 完整工作流程

用户操作/事件触发

   SDK 采集数据

  添加到内存缓存

【自动保存到 LocalStorage】✅ 新增

   检查网络状态

  ┌─────┴─────┐
在线          离线
  ↓            ↓
批量上报    【暂存本地】✅ 新增
  ↓            ↓
成功/失败   等待网络恢复
  ↓            ↓
清空缓存   【自动上报】✅ 新增

          清空缓存

5. ⭐ SSE 实时推送(项目亮点)

采用 Server-Sent Events (SSE) 技术实现服务端主动推送,为监控系统提供实时数据流。

🎯 为什么使用 SSE?

相比传统的 HTTP 轮询和 WebSocket,SSE 更适合监控系统:

特性HTTP 轮询WebSocketSSE
通信方向客户端拉取双向单向推送
协议HTTPWS/WSSHTTP/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 带来的优势

  1. 实时性强

    • 毫秒级延迟
    • 数据产生后立即推送
    • 无需前端轮询
  2. 资源占用低 💰

    • 单个 HTTP 长连接
    • 减少 99% 的无效请求
    • 服务器压力小
  3. 实现简单 🛠️

    • 基于标准 HTTP 协议
    • 浏览器原生支持
    • 无需额外的库
  4. 自动重连 🔄

    • 连接断开自动重连
    • 无需手动处理
    • 保证数据可靠性
  5. 灵活推送 🎯

    • 支持多种事件类型
    • 按需订阅
    • 分类推送
  6. 实时告警 🚨

    • 慢请求立即通知
    • 错误实时告警
    • 异常及时响应

📈 性能对比

方案请求次数/分钟平均延迟服务器压力实时性
轮询 (3秒)20次1.5秒
长轮询10次1秒
WebSocket1个连接<100ms
SSE1个连接<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 的智能上报方案结合了:

  1. 批量聚合 - 减少请求次数
  2. 智能选择 - 根据数据大小选择最优方式
  3. 空闲上报 - 不阻塞用户操作
  4. 离线持久化 - LocalStorage 本地暂存,确保数据不丢失 ✨ 新增
  5. 自动恢复 - 网络恢复时自动上报离线数据 ✨ 新增
  6. SSE 推送 - 实时数据流,毫秒级延迟
  7. 自动重连 - 保证数据可靠性
  8. 多端同步 - 多个监控面板实时同步

这些优化使得 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));

Released under the MIT License.