一文搞定前端埋点之上报数据

背景

随着互联网产品精细化运营需求的提升,用户行为数据成为优化产品体验、制定商业策略的核心依据。而为了让用户取得上述数据满足对应需求,有了埋点的概念。

前端埋点通过采集用户交互行为(如点击、曝光、停留时长)、性能指标(如页面加载时间)及异常信息,构建了从用户视角出发的数据链路。而SDK作为埋点体系的中枢,需将海量数据高效、可靠地传输至服务端,以供后续清洗、分析与可视化。这一过程的稳定性与效率直接决定了数据价值的可用性。

因此,数据传输,或者说上报的逻辑在整个埋点项目中起着至关重要的作用。这篇文章将带你从思路到代码一步一步实现sdk对服务端的数据上报~~

本篇侧重于上报板块,所以可参考以下文章来对前端埋点SDK有一个基本认识:

  1. 埋点认识与开发层面------大家都该学学的埋点概念与使用

  2. 埋点应用层面------企业级埋点教学!到底多重要一看就知道

学到什么

  1. 不同上报方式的选择与优劣

  2. SDK上报的基本思路

  3. 根据思路实现上报逻辑代码

  4. 上报优化思路和简单实现

正文

传统理解的上报可能就是发发请求,调用接口。但是真实的开发中远远没有那么简单。比如我们会根据数据的紧急程度选择实时上报和批量上报两种方式,应当如何实现呢?如果选择批量上报形式,暂时不上报的数据应该怎么存储呢?再比如发请求这个步骤,是发xhr请求还是别的请求方式呢?要回答这些问题,就需要在上报思路前先了解一些前置知识。

一、前置知识补充

1.1怎么实现两种上报形式?------两种队列

通过这张图我们可以清晰的看到埋点sdk行为数据的上报分为实时上报和延迟批量上报两种形式,那么必然会涉及到数据的临时存储,这里向大家介绍的方式是用数组模仿队列 存储数据,实时队列和批量队列分别对应实时上报和批量上报。

这里知道有两种不同的队列即可,后面会详细介绍维护方式和数据处理方式。

1.2 怎么队列化?------统一数据结构和事件

有了队列必然涉及到存储的数据结构,也就是我们怎么将获取到的不同类型的数据放入队列中。它的确定一方面涉及到和server端的沟通(上报接口的设计),另一方面也要为了上报代码尽量简洁。这里推荐统一上报接口的形式。

举个例子:

如果行为监控、错误监控和性能监控分别开发上报接口,在批量上报的情况下,取出的既有行为监控的数据又有错误监控的数据,那么就要针对数据进行判断以此确定调用哪个接口;如果是统一接口,那么所有数据都可以直接调用此上报接口进行调用,比较方便和简洁。

json 复制代码
{
    "eventType": "performance",
    "timestamp": 1678271400000,
    "pageUrl": "https://example.com/home",
    "eventData": 
    {
      //对应数据
    }
}

有了标准统一的接口我们就可以将来自不同监控的数据改造为统一数据结构的事件然后顺利存入队列中啦~

1.3 怎么发送?------三种发送方式及对比

有了队列和存储形式,终于来到了最重要的发送环节也就是请求环节。以下是不同上报方式以及优劣对比。

关于三种上报的具体解析,可参考文章:教你前端怎么实现埋点上报

在本项目中,采取**"用户自定义+sendBeacon优先"**的上报方式,以下是上报方式的选择代码

php 复制代码
selectStrategy(isImmediate: boolean, reportStrategy: ReportStrategy | undefined | 'auto'): ReportStrategy {
    if (isImmediate) {
      return this.supportBeacon() ? 'BEACON' : 'XHR';//自动上报判断是否支持sendBeacon,xhr兜底
    }
    switch (reportStrategy) {//手动上报遵循用户选择的上报方式,同样xhr兜底
      case 'BEACON':
        return 'BEACON';
      case 'XHR':
        return 'XHR';
      case 'IMG':
        return 'IMG';
      default:
        return 'XHR';
    }
  }

有了上述知识,相信小伙伴们已经对SDK上报有了基本的知识,但是脑子里可能还是乱乱的,没有一个清晰的思路。所以接下来让我们一起来看看SDK上报的具体思路和实现吧!

二、SDK上报的基本思路及代码实现

由前面补充知识不难看出,SDK上报的核心无非在三个模块:数据队列化模块、队列管理模块、上报策略模块,那么由此可以推理出一次上报的步骤:

  1. 将来自不同监控的数据通过数据队列化模块处理为统一存储的事件

  2. 将事件根据不同的上报时机入队(实时上报和批量上报)

  3. 将发送队列从队列管理模块中取出并从上报策略模块选择对应的上报策略

  4. 发送队列并将发送过的事件出队,至此完成一次基本的发送封装

  5. 基于基本的发送,还可以对应封装出针对不同的监控的自动上报函数

下面是三个模块的对应图表演示和代码解析,将对应函数比喻为state和setstate等帮助大家理解。

2.1 EvevtManager模块:

用于导出函数对数据进行队列化处理

typescript 复制代码
export const createBaseEvent = (eventType: string, eventData?: Record<string, any>) => {
  return {
    eventType,
    ...eventData,
    timestamp: Date.now(),
    pageUrl: window.location.href,
  };
};//对应上报接口的数据结构

2.2 QueueManager模块:

用于管理队列状态

kotlin 复制代码
//统一的事件类型
import { TrackEvent } from './types';
export class QueueManager {
  //立即上报队列
  immediateQueue: TrackEvent[];
  //批量上报队列 
  batchQueue: TrackEvent[]; 
  //初始化
  constructor() {
    this.immediateQueue = [];
    this.batchQueue = [];
  }
  //入队操作:根据传入的isImmediate和是否为关键事件决定进入的的队列类型
  enqueueEvent(event: TrackEvent, isImmediate: boolean) {
    if (isImmediate || this.isCriticalEvent(event)) {
      this.immediateQueue.push(event);
    } else {
      this.batchQueue.push(event);
    }
  }
  private isCriticalEvent(event: TrackEvent): boolean {
    return ['error', 'purchase', 'checkout', 'behavior_pv'].includes(event.eventType);
  }
  //取出发送队列:根据isImmediate和limit决定返回的队列类型和长度,并在取出后立即出队
  flushQueue(isImmediate: boolean, limit?: number): TrackEvent[] {
    const queue = isImmediate ? this.immediateQueue : this.batchQueue;
    if (!isImmediate) {
      const events = limit ? queue.slice(0, limit) : queue;
      this.batchQueue = queue.slice(limit);
      return events;
    }
    this.immediateQueue = [];
    return queue;
  }
}

2.3 StretageManager模块:

用于管理上报方式

typescript 复制代码
import { TrackEvent } from './types';
export class StretageManager {
   //三种具体上报方式
   //sendBeacon发送
  private sendWithBeacon(events: TrackEvent[], endpoint: string) {
    const blob = new Blob([JSON.stringify(events)], {
      type: 'application/json',
    });
    navigator.sendBeacon(endpoint, blob);
  }
   //传统XHR发送
  private sendWithXHR(events: TrackEvent[], endpoint: string) {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', endpoint);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(JSON.stringify(events));
  }
   //IMG发送
  private sendWithImage(events: TrackEvent[], endpoint: string) {
    const params = new URLSearchParams();
    params.set('data', btoa(JSON.stringify(events)));
    const img = new Image();
    img.src = `${endpoint}?${params}`;
    img.onload = img.onerror = () => img.remove();
  }
  //根据参数选择上报方式
  selectStrategy(isImmediate: boolean, reportStrategy){
    if (isImmediate) {
      return this.supportBeacon() ? 'BEACON' : 'XHR';
    }
    switch (reportStrategy) {
      case 'BEACON':
        return 'BEACON';
      case 'XHR':
        return 'XHR';
      case 'IMG':
        return 'IMG';
      default:
        return 'XHR';
    }
  }
  //辅助函数查询是否支持sendBeacon
  private supportBeacon(): boolean {
    return typeof navigator.sendBeacon === 'function';
  }
  //根据上报方式进行实际上报
  sendBatch(events: TrackEvent[], strategy: ReportStrategy, endpoint: string) {
    try {
      switch (strategy) {
        case 'BEACON':
          this.sendWithBeacon(events, endpoint);
          break;
        case 'XHR':
          this.sendWithXHR(events, endpoint);
          break;
        case 'IMG':
          this.sendWithImage(events, endpoint);
          break;
      }
    } catch (error) {
      console.error('上报失败,重新入队', events);
      //重新入队逻辑
    }
  }
}

2.4 主tracker类:

用于为用户(手动)以及sdk本身三类监控(自动)封装不同的上报函数,完成自动上报和批量上报的逻辑,并进行适当优化如页面卸载前上报完两个队列的所有数据等等。

typescript 复制代码
//引入三个主要模块
import { QueueManager } from './QueueManager';
import { StretageManager } from './StrategeManager';
import { createBaseEvent } from './EventManager';
export class Tracker {
  private config: TrackerConfig;//传入的参数
  private readonly BATCH_INTERVAL = 5000;//批量上报时间差
  private readonly BATCH_LIMIT = 20;//批量上报的数据量
  private queueManager = new QueueManager();//初始化队列管理类
  private stretageManager = new StretageManager();//初始化上报策略管理类
  constructor(config: TrackerConfig) {
    this.config = {
      autoTrack: {
        pageView: true,
        click: true,
        performance: true,
        ...config.autoTrack,
      },
      reportStrategy: 'auto',
      ...config,//覆盖对应默认参数
    };
    this.initBatchFlush();//初始化批量上报
    this.initPageUnload();//页面卸载前清空队列并发送
  }
  //为用户封装上报逻辑:创建事件->入队->上报
  public trackEvent = (eventType: string, isImmediate = false, eventData?: Record<string, any>, limit?: number) => {
    //创建事件
    const event = createBaseEvent(eventType, eventData);
    //入队:通过调用队列管理模块的方法
    this.queueManager.enqueueEvent(event, isImmediate);
    //上报:通过调用策略管理模块的函数
    if (isImmediate) {
      this.stretageManager.sendBatch(this.queueManager.flushQueue(true, limit), this.stretageManager.selectStrategy(true, this.config.reportStrategy), this.config.endpoint);
    }
  };
  //为三类监控封装上报逻辑
  //性能自动上报
  public reportPerformance = (data: Record<string, any>) => {
    this.trackEvent('performance', false, data);
  };
  //行为自动上报
  public reportBehavior(type: string, data: Record<string, any>, immediate = type === 'pv') {
    this.trackEvent(`behavior_${type}`, immediate, data);
  }
  //错误自动上报
  public reportError(error: Error | string, extra?: Record<string, any>) {
    this.trackEvent('error', true, errorData);
  }

  //批量上报:间隔时间取出批量队列中固定长度的子队列进行上报
  private initBatchFlush() {
    setInterval(() => {
      // 直接处理批量队列中的存量事件
      const events = this.queueManager.flushQueue(false, this.BATCH_LIMIT);
      if (events.length > 0) {
        this.stretageManager.sendBatch(events, this.stretageManager.selectStrategy(false, this.config.reportStrategy), this.config.endpoint);
      }
    }, this.BATCH_INTERVAL);
  }
  //优化:
  //页面卸载前上报避免数据丢失
  private initPageUnload() {
    window.addEventListener('beforeunload', () => {
      this.trackEvent('snedAllBatch', false);
      this.trackEvent('sendAllImeediate', true);
    });
  }
}

以上就是sdk上报tracker类完整的代码示例啦,为了让大家更直观的理解和使用,下面编写一个使用demo供大家参考。

三、最小demo演示

基本使用思路为:初始化traker类 -> 在三类监控中传入上报函数(自动)/用户在页面调用函数(手动),以下为代码演示:

3.1 sdk内部使用(自动上报):以性能监控为例

javascript 复制代码
import { Tracker } from 'xxx';//上报类的引入
import { performanceTracking } from 'xxx';//性能监控的引入
//初始化
tracker = new Tracker({
      endpoint: config.reportUrl,
      autoTrack: {
        pageView: true,
        click: true,
        performance: true,
      },
    });
//将tracker类中的函数作为回调传给各个监控
 performanceTracking.init(data => {
      tracker.reportPerformance(data);
    });
  }

3.2 用户使用(手动上报):以React的button按钮为例

javascript 复制代码
import { Tracker } from 'xxx';//上报类的引入
export default function DemoApp() {
  tracker = new Tracker({
      endpoint: config.reportUrl,
      autoTrack: {
        pageView: true,
        click: true,
        performance: true,
      },
      reportStrategy: 'XHR'
    });
   const handleClick=()=>{
       tracker.trackEvent('ButtonEvent',true)//上报按钮事件并指定立即上报
   }
  return (
    <div>
      <button onClick={handleClick}>点我上报测试</button>
    </div>
  );
}

四、总结

以下表格是高并发(100次/s)场景下新旧两种方案各项指标的测试:

读表可以看到通过双队列管理和动态上报策略,SDK实现了数据上报的高效性与可靠性。实际开发中需结合业务需求选择上报方式:关键行为(如支付、错误)优先实时上报,常规行为(如页面浏览)通过批量队列减少请求频率。此外,结合本地缓存和失败重试机制,可进一步提升数据完整性。

优化

本篇文章只是一个简单的实现,还有很多可以优化的地方。

  1. 上报失败重新入队(避免数据丢失)
  2. 单请求批量上报,单位时间内的请求按照一个 http 请求合体发送(进一步压缩请求次数)

这里选取重新入队的逻辑进行示范:

首先在队列管理模块增加重入队函数

vbnet 复制代码
//优化:重新入队  reEnqueue(events: TrackEvent | TrackEvent[]) {    const normalized = Array.isArray(events) ? events : [events];    normalized.forEach(event => {      if (event.attempts < 2) {        event.attempts += 1;        this.enqueueEvent(event, this.isCriticalEvent(event));      }    });  }

然后在主类中将重入队函数注入上报策略模块使用

kotlin 复制代码
private stretageManager = new StretageManager(this.queueManager.reEnqueue.bind(this.queueManager));
相关推荐
好_快2 分钟前
Lodash源码阅读-keys
前端·javascript·源码阅读
亿牛云爬虫专家3 分钟前
Headless Chrome 优化:减少内存占用与提速技巧
前端·chrome·内存·爬虫代理·代理ip·headless·大规模数据采集
好_快5 分钟前
Lodash源码阅读-arrayFilter
前端·javascript·源码阅读
若云止水6 小时前
ngx_conf_handler - root html
服务器·前端·算法
佚明zj6 小时前
【C++】内存模型分析
开发语言·前端·javascript
知否技术7 小时前
ES6 都用 3 年了,2024 新特性你敢不看?
前端·javascript
最初@8 小时前
el-table + el-pagination 前端实现分页操作
前端·javascript·vue.js·ajax·html
知否技术9 小时前
JavaScript中的闭包真的过时了?其实Vue和React中都有用到!
前端·javascript
Bruce_Liuxiaowei9 小时前
基于Flask的防火墙知识库Web应用技术解析
前端·python·flask
zhu_zhu_xia9 小时前
vue3中ref和reactive的差异分析
前端·javascript·vue.js