4.前端监控---数据上报

56 阅读3分钟

1. 概述

数据上报是埋点系统的核心环节,负责将采集到的用户行为数据发送到服务器。一个高效的数据上报方案需要解决以下问题:

  1. 上报时机:何时触发数据上报。
  2. 上报方式:如何发送数据(如 HTTP 请求、WebSocket、图片打点等)。
  3. 数据压缩:如何减少数据体积,提升传输效率。
  4. 数据格式:上报数据的结构和内容。
  5. 性能优化:如何减少对页面性能的影响。
  6. 错误处理:如何处理上报失败的情况。

2. 上报时机

2.1 实时上报

在用户行为触发时立即上报数据。适用于关键事件(如支付成功)。

判断依据:

  • 关键事件:如支付成功、表单提交等。
  • 高优先级数据:如错误日志、性能指标等。

示例代码:

function trackCriticalEvent(eventType, eventData) {
  const trackingData = {
    eventType,
    ...eventData,
    timestamp: new Date().toISOString(),
  };
  sendTrackingData(trackingData); // 实时上报
}

2.2 延迟上报

在页面空闲时上报数据。适用于非关键事件(如点击、滚动)。

判断依据:

  • 非关键事件:如点击、滚动、页面浏览等。
  • 低优先级数据:如用户行为日志、页面停留时间等。

示例代码:

function trackNonCriticalEvent(eventType, eventData) {
  const trackingData = {
    eventType,
    ...eventData,
    timestamp: new Date().toISOString(),
  };
  scheduleReport(trackingData); // 延迟上报
}

function scheduleReport(data) {
  if (window.requestIdleCallback) {
    window.requestIdleCallback(() => sendTrackingData(data));
  } else {
    setTimeout(() => sendTrackingData(data), 5000); // 延迟5秒上报
  }
}

2.3 页面离开时上报

在用户离开页面时上报数据。适用于记录页面停留时间等场景。

判断依据:

  • 页面离开事件:如关闭页面、刷新页面、跳转到其他页面等。

示例代码:

function trackPageLeave() {
  const pageStayTime = Date.now() - this.pageEnterTime; // 计算页面停留时间
  const trackingData = {
    eventType: 'page_leave',
    pageStayTime,
    timestamp: new Date().toISOString(),
  };
  sendTrackingData(trackingData); // 页面离开时上报
}

window.addEventListener('beforeunload', () => trackPageLeave());

3. 上报方式

3.1 使用 fetch 发送数据

fetch 是现代浏览器推荐的网络请求方式,支持 Promise 和异步操作。

示例代码:

function sendTrackingData(data) {
  fetch('/api/track', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  })
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      console.log('Data reported successfully');
    })
    .catch(error => {
      console.error('Failed to report data:', error);
    });
}

3.2 使用 navigator.sendBeacon 发送数据

navigator.sendBeacon 是专为数据上报设计的 API,适用于页面离开时的上报。

示例代码:

function sendTrackingData(data) {
  const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
  if (navigator.sendBeacon('/api/track', blob)) {
    console.log('Data reported successfully');
  } else {
    console.error('Failed to report data');
  }
}

3.3 使用 WebSocket 发送数据

WebSocket 适用于需要实时上报数据的场景。

示例代码:

const socket = new WebSocket('wss://example.com/track');

socket.onopen = () => {
  console.log('WebSocket connection established');
};

socket.onerror = (error) => {
  console.error('WebSocket error:', error);
};

function sendTrackingData(data) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify(data));
  } else {
    console.error('WebSocket is not open');
  }
}

3.4 使用图片打点发送数据

图片打点是一种传统的上报方式,适用于跨域场景。

示例代码:

function sendTrackingData(data) {
  const params = new URLSearchParams(data);
  const img = new Image();
  img.src = `https://example.com/track?${params.toString()}`;
}

4. 数据压缩

4.1 压缩算法选择

常用的压缩算法包括 GzipDeflate 和 Brotli。以下是它们的对比和推荐场景:

特性GzipDeflateBrotli
压缩率较高与 Gzip 相似更高(比 Gzip 高 20%-26%)
压缩速度较快较快较慢(压缩时间比 Gzip 长)
解压速度
浏览器支持所有现代浏览器所有现代浏览器现代浏览器(IE 不支持)
适用场景通用场景通用场景静态资源、文本数据
HTTP 头部支持Content-Encoding: gzipContent-Encoding: deflateContent-Encoding: br

推荐场景:

  • 动态内容:Gzip。
  • 静态资源:Brotli。
  • 兼容性要求高:Gzip。

4.2 客户端压缩

在客户端压缩数据,推荐使用 Gzip。

示例代码:

async function compressData(data) {
  const stream = new Blob([JSON.stringify(data)]).stream();
  const compressedStream = stream.pipeThrough(new CompressionStream('gzip'));
  return await new Response(compressedStream).blob();
}

async function sendTrackingData(data) {
  const compressedData = await compressData(data);
  fetch('/api/track', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Encoding': 'gzip',
    },
    body: compressedData,
  })
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      console.log('Data reported successfully');
    })
    .catch(error => {
      console.error('Failed to report data:', error);
    });
}

4.3 服务端压缩

在服务端压缩数据,推荐使用 Brotli 或 Gzip。

示例代码(Node.js):

const zlib = require('zlib');

function compressData(data, algorithm = 'gzip') {
  return new Promise((resolve, reject) => {
    const compress = algorithm === 'brotli' ? zlib.brotliCompress : zlib.gzip;
    compress(JSON.stringify(data), (err, buffer) => {
      if (err) reject(err);
      else resolve(buffer);
    });
  });
}

// 使用示例
compressData({ event: 'click', target: 'button#submit' }, 'brotli')
  .then(compressedData => {
    console.log('Compressed Data:', compressedData);
  })
  .catch(error => {
    console.error('Compression failed:', error);
  });

5. 数据格式

5.1 数据结构

上报数据应包含以下字段:

  • 事件类型:如 clickscrollpage_leave
  • 事件目标:如 button#submit
  • 时间戳:事件发生的时间。
  • 页面信息:如页面 URL、页面标题。
  • 设备信息:如设备类型、屏幕分辨率。
  • 环境信息:如网络类型、语言设置。

示例数据:

{
  "eventType": "click",
  "eventTarget": "button#submit",
  "timestamp": "2023-10-01T12:34:56.789Z",
  "pageUrl": "https://example.com/home",
  "pageTitle": "Home Page",
  "deviceType": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
  "screenResolution": "1920x1080",
  "networkType": "4g",
  "language": "zh-CN"
}

6. 性能优化

6.1 批量上报

将多个事件数据缓存后,一次性上报。

示例代码:

class Tracker {
  constructor() {
    this.queue = []; // 数据缓存队列
    this.isSending = false; // 是否正在发送数据
  }

  // 添加数据到队列
  addToQueue(data) {
    this.queue.push(data);
    this.processQueue();
  }

  // 处理队列
  processQueue() {
    if (this.isSending || this.queue.length === 0) return;

    this.isSending = true;
    const batchData = this.queue.splice(0, 10); // 每次发送10条数据

    this.sendTrackingData(batchData).finally(() => {
      this.isSending = false;
      this.processQueue(); // 继续处理剩余数据
    });
  }

  // 发送数据
  async sendTrackingData(data) {
    try {
      await fetch('/api/track', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      });
    } catch (error) {
      console.error('Failed to send tracking data:', error);
      // 将失败的数据重新加入队列
      this.queue.unshift(...data);
    }
  }
}

6.2 延迟上报

在页面空闲时上报数据,减少对用户操作的影响。

示例代码:

function scheduleReport(data) {
  if (window.requestIdleCallback) {
    window.requestIdleCallback(() => sendTrackingData(data));
  } else {
    setTimeout(() => sendTrackingData(data), 5000); // 延迟5秒上报
  }
}

7. 错误处理

7.1 重试机制

在网络请求失败时,自动重试上报。

示例代码:

async function sendTrackingData(data, retryCount = 3) {
  try {
    await fetch('/api/track', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });
  } catch (error) {
    if (retryCount > 0) {
      console.log(`Retrying... (${retryCount} attempts left)`);
      setTimeout(() => sendTrackingData(data, retryCount - 1), 1000); // 1秒后重试
    } else {
      console.error('Failed to report data after retries:', error);
    }
  }
}

7.2 本地存储

在网络不可用或上报失败时,将数据存储到本地,待网络恢复后重新上报。

示例代码:

function logToLocalStorage(data) {
  const logs = JSON.parse(localStorage.getItem('trackingLogs') || '[]');
  logs.push(data);
  localStorage.setItem('trackingLogs', JSON.stringify(logs));
}

function retryFailedReports() {
  const logs = JSON.parse(localStorage.getItem('trackingLogs') || '[]');
  if (logs.length > 0) {
    sendTrackingData(logs).then(() => {
      localStorage.removeItem('trackingLogs');
    });
  }
}

// 每隔5分钟尝试重新上报
setInterval(retryFailedReports, 5 * 60 * 1000);

8. 总结

本文档提供了一套完整的数据上报方案,包括:

  1. 上报时机:实时上报、延迟上报、页面离开时上报。
  2. 上报方式:使用 fetchnavigator.sendBeacon、WebSocket 和图片打点。
  3. 数据压缩:使用 Gzip 或 Brotli 压缩数据。
  4. 数据格式:规范化的数据结构。
  5. 性能优化:批量上报和延迟上报。
  6. 错误处理:重试机制和本地存储。