前端埋点全解及埋点SDK实现方式

一、什么是埋点

所谓"埋点",是数据采集领域(尤其是用户行为数据采集领域)的术语,指的是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。比如用户某个icon点击次数、观看某个视频的时长等等。

埋点的技术实质,是先监听软件应用运行过程中的事件,当需要关注的事件发生时进行判断和捕获。埋点需要在特定的时机来收集数据,然后上报给服务端进行分析

二、为什么需要埋点

数据生产-》数据采集-》数据处理-》数据分析挖掘-》数据驱动/用户反馈-》产品优化/迭代

上述是整个数据从产生到最终作用于产品优化上的过程。埋点是整个流程的开始点,终端提供商在收集到埋点数据之后,通过大数据处理、数据统计、数据分析、数据挖掘等加工处理,可以得到衡量产品状态的一些基本指标,比如活跃、留存、新增等大盘数据,从而洞察产品的状态,随着数据挖掘等技术的兴起,埋点采集到的数据在以下方面的作用也越来越凸显。

三、如何去埋点

一般情况下,主要有3类埋点:展现埋点 + 曝光埋点 + 交互埋点。
1、展现埋点

定义展现其实是一个服务端的触发。服务端被触发后,用户侧将会展现什么内容,展现埋点需要记录的是页面展现的内容信息,即服务端下发的内容是什么(这些东西一定是当前页面主要内容,不包含一些交互信息)。
2、曝光埋点

哪些下发的内容被用户实际看到了。和展现埋点类似,由于屏幕有限,但内容可以无限。哪些内容被用户侧实际看到(曝光),需要记录的是单个"内容"被看到。一系列被下发的内容,可以触发多次曝光埋点。
3、交互埋点

交互埋点表明的是功能/内容被用户"点击"了。从埋点时机来说,这个是展现 & 曝光的下游。记录对于我们提供的"服务"的"消费"情况。比如,一个页面,用户可以点击,那么我们需要记录相应的交互动作埋点;比如,一个视频可以点赞,我们也可以记录交互埋点;比如,一个视频可以播放暂停,我们也可以记录消费埋点。总体来说,就是,我们要记录 被看到的可交互功能/信息的"消费"数据。

四、埋点记录

关于埋点记录需要明确记录两个信息:点位信息、点位映射。

点位信息:明确每个业务事件下的具体的参数信息,包含公共参数、业务参数。

点位映射:每个埋点对应的业务含义。

五、前端监控

一般来讲一个成熟的产品,运营与产品团队需要关注用户在产品内的行为记录,通过用户的行为记录来优化产品,研发与测试团队则需要关注产品的性能以及异常,确保产品的性能体验以及安全迭代。

前端监控一般也分为三大类:

5.1 数据监控(监控用户行为)

●PV/UV: PV(page view):即页面浏览量或点击量;UV:指访问某个站点或点击某条新闻的不同 IP 地址的人数

●用户在每一个页面的停留时间

●用户通过什么入口来访问该网页

●用户在相应的页面中触发的行为,等...

统计这些数据是有意义的,比如我们知道了用户来源的渠道,可以促进产品的推广,知道用户在每一个页面停留的时间,可以针对停留较长的页面,增加广告推送等等。

5.2 性能监控(监控页面性能)

●不同用户,不同机型和不同系统下的首屏加载时间

●白屏时间

●http 等请求的响应时间

●静态资源整体下载时间

●页面渲染时间

●页面交互动画完成时间,等...

这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,尽可能的提高用户体验。

5.3 异常监控(监控产品、系统异常)

及时的上报异常情况,可以避免线上故障的发上。虽然大部分异常可以通过 try catch 的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获。常见的需要监控的异常包括:

●Javascript 的异常监控

●样式丢失的异常监控

六、埋点上报

实现前端监控,第一步肯定是将我们要监控的事项(数据)给收集起来,再提交给后台进行入库,最后再给数据分析组进行数据分析,最后处理好的数据再同步给运营或者是产品。数据收集的丰富性和准确性会直接影响到我们做前端监控的质量,因为我们会以此为基础,为产品的未来发展指引方向。

现在常见的埋点上报方法有三种:手动埋点、可视化埋点、无埋点

6.1 手动埋点

手动埋点,也叫代码埋点,即纯手动写代码,调用埋点 SDK 的函数,在需要埋点的业务逻辑功能位置调用接口,上报埋点数据,像**[友盟] [百度统计]**等第三方数据统计服务商大都采用这种方案。手动埋点让使用者可以方便地设置自定义属性、自定义事件;所以当你需要深入下钻,并精细化自定义分析时,比较适合使用手动埋点。

手动埋点的缺陷就是,项目工程量大,需要埋点的位置太多,而且需要产品开发运营之间相互反复沟通,容易出现手动差错,如果错误,重新埋点的成本也很高。

6.2 可视化埋点

通过可视化交互的手段,代替上述的代码埋点。将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码。

可视化埋点的缺陷就是可以埋点的控件有限,不能手动定制。

6.3 无埋点

无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据。优点是前端只要一次加载埋点脚本,缺点是流量和采集的数据过于庞大,服务器性能压力山大。

佳佳

HTML

COBUB

PHP

无码埋点

七、SDK实现

数据收集以及数据上报的方式都有了,那么就可以开始实现SDK了,这里先以单文件(非工程化)进行实现

7.1 非工程化

复制代码
// JS 完整代码部分
(function (e) {
  function wrap(event) {
    const fun = history[event];
    return function () {
      const res = fun.apply(this, arguments);

      const e = new Event(event);

      window.dispatchEvent(e);

      return res;
    };
  }

  class TrackingDemo {
    constructor(options = {}) {
      // 重写 pushState、replaceState
      window.history.pushState = wrap("pushState");
      window.history.replaceState = wrap("replaceState");

      // 上报地址
      this.reportUrl = options.reportUrl || "";
      this.sdkVersion = "1.0.0";

      this._eventList = ["click", "dblclick", "mouseout", "mouseover"];

      this._dulation = {
        startTime: 0,
        value: 0,
      };
      this._initJSError();

      // 初始化事件数据收集
      this._initEventHandler();

      // 初始化PV统计
      this._initPV();

      this._initPageDulation();
    }

    setUserId(uid) {
      this.uid = uid;
    }

    _initEventHandler() {
      this._eventList.forEach((event) => {
        window.addEventListener(event, (e) => {
          const target = e.target;
          const reportKey = target.getAttribute("report-key");
          if (reportKey) {
            this._report("event", {
              tagName: e.target.nodeName,
              tagText: e.target.innerText,
              event,
            });
          }
        });
      });
    }

    _initPV() {
      window.addEventListener("pushState", (e) => {
        this._report("pv", {
          type: "pushState",
          referrer: document.referrer,
        });
      });

      window.addEventListener("replaceState", (e) => {
        this._report("pv", {
          type: "replaceState",
          referrer: document.referrer,
        });
      });

      window.addEventListener("hashchange", () => {
        this._report("pv", {
          type: "hashchange",
          referrer: document.referrer,
        });
      });
    }

    _initPageDulation() {
      let self = this;

      function initDulation() {
        const time = new Date().getTime();
        self._dulation.value = time - self._dulation.startTime;

        self._report("dulation", {
          ...self._dulation,
        });

        self._dulation.startTime = time;
        self._dulation.value = 0;
      }

      // 首次进入页面
      window.addEventListener("load", () => {
        // 记录时间
        const time = new Date().getTime();
        this._dulation.startTime = time;
      });

      // 单页应用页面跳转(触发 replaceState)
      window.addEventListener("replaceState", () => {
        initDulation();
      });

      // 单页应用页面跳转(触发 pushState)
      window.addEventListener("pushState", () => {
        initDulation();
      });

      // 非单页应用跳转触发 popstate
      window.addEventListener("popstate", () => {
        initDulation();
      });

      // 页面没有任何跳转, 直接关闭页面的情况
      window.addEventListener("beforeunload", () => {
        initDulation();
      });
    }

    _initJSError() {
      window.addEventListener("error", (e) => {
        this._report("error", {
          message: e.message,
        });
      });

      window.addEventListener("unhandledrejection", (e) => {
        this._report("error", {
          message: e.reason,
        });
      });
    }

    // 用户可主动上报
    reportTracker(data) {
      this._report("custom", data);
    }

    _getPageInfo() {
      const { width, height } = window.screen;
      const { userAgent } = navigator;
      return {
        uid: this.uid,
        title: document.title,
        url: window.location.href,
        time: new Date().getTime(),
        ua: userAgent,
        screen: `${width}x${height}`,
      };
    }

    _report(type, data) {
      const reportData = {
        ...this._getPageInfo(),
        type,
        data,
        sdk: this.sdkVersion,
      };

      if (navigator.sendBeacon) {
        navigator.sendBeacon(this.reportUrl, JSON.stringify(reportData));
      } else {
        const imgReq = new Image();
        imgReq.src = `${this.reportUrl}?params=${JSON.stringify(
          reportData
        )}&t=${new Date().getTime()}`;
      }
    }
  }
  e.TrackingDemo = TrackingDemo;
})(window);
// HTML 代码部分
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button report-key="button">按钮</button>
</body>
<script src="./tackerDemo.js"></script>
<script>
    const trackingDemo = new TrackingDemo()
</script>
</html>

实现方式就是将此文中 数据收集 部分的代码整合成一个类。

7.2工程化

如果项目是模块化开发的话,就需要打包工具进行打包,以便支持esm、cjs和umd三种方式,同时工程化还便于维护。

这里使用rollup进行打包,以及使用typescript进行类型约束。

GitHub - GaLiDunGuide/TrackerDemo

7.3 发布NPM包

复制代码
"files": ["dist"],  // 发布 npm 时需要上传的目录
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"browser": "dist/index.js",

八、为什么都用GIF来做埋点?

向服务器端上报数据,可以通过请求接口,请求普通文件,或者请求图片资源的方式进行。只要能上报数据,无论是请求GIF文件还是请求js文件或者是调用页面接口,服务器端其实并不关心具体的上报方式。那为什么所有系统都统一使用了请求GIF图片的方式上报数据呢?

●防止跨域

一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。而跨域请求很容易出现由于配置不当被浏览器拦截并报错,这是不能接受的。但图片的src属性并不会跨域,并且同样可以发起请求。(排除接口上报)

●防止阻塞页面加载,影响用户体验

通常,创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。反复操作DOM不仅会引发性能问题,而且载入js/css资源还会阻塞页面渲染,影响用户体验。

但是图片请求例外。构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点,这是其他类型的资源请求所做不到的。(排除文件方式)

●相比PNG/JPG,GIF的体积最小

最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节。

同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。
并且大多采用的是1*1像素的透明GIF来上报

1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax