手把手教你搭建一个无框架埋点体系

发表于 3年以前  | 总阅读数:208 次

背景

埋点体系构成

一般来说,一个完整的埋点体系由以下三个部分构成:

  • 应用
  • 数据分析平台
  • 数据平台 SDK

埋点上报是将应用层事件上传至上层平台的过程。比方说,在某购物网站上,用户点击了「收藏」按钮,此时,一个点击事件就生成了,这一事件会被上报至一个数据分析平台。这样,相关的数据分析师、产品经理、运营等同学便可以在数据分析平台,通过这些上报的事件数据分析,得出应用中可以优化的方方面面。由此可见,埋点上报是每个产品走向卓越的重要一环。

通过以上描述,我们认识了埋点上报过程的两大主角:应用与数据分析平台。从前端技术的角度来说,我们通常还需要第三个角色的助攻,那就是数据平台 SDK. 这个 SDK 封装了数据分析平台的各种接口,暴露出简单的方法让我们进行调用,实现简易的埋点上传。

两种埋点事件

我们可以把应用层事件分为两大类:

  • 「页面事件」:一种是通用的「页面事件」,比如说用户在应用某个页面的停留及活跃时长,我们希望这种全局的埋点只用在项目初始化时注入一次,不需要在代码中进行维护。
  • 「触发事件」:另一种则是自定义的「触发事件」,比如点击某个特定的按钮,开启某个特定的流程,这种事件需要前端同学在代码中手动注入埋点。

我们为这两种事件分别开发了一套埋点上传 SDK。下面,我们就来详细地讲解一下这两套 SDK 的技术知识。

处理「页面事件」的 SDK - monitor-tracer

monitor-tracer 是一个用来监控页面及组件可见时长和活跃时长的前端 SDK,同时也是 Monitor 前端埋点体系的一个核心组成部分。

背景

为了更好地理解用户对各个业务功能的使用状况,从而进行相应的产品优化和调整:

  • 对于一般的网页应用,我们需要对用户在应用某个页面的停留及活跃时长进行相应的统计;

  • 对于大盘 / 看板 / dashboard 类型的页面(如下图所示),我们希望在页面维度的基础上,更进一步地统计每个组件对用户的可见时长,从而对它们的排列顺序和内容进行优化。

    一个 dashboard 类页面

基于以上需求,我们开发了 monitor-tracer SDK, 旨在实现对 「页面可见、活跃时长」「组件可见时长」 的统计。

名词解释

  • 页面 (Page) - 在浏览器中打开的网页,不同页面以路径 location.pathname 来作区分;
  • 页面可见时长 - 一个页面对用户可见的累计时长;
  • 页面活跃时长 - 用户在页面上进行有效的鼠标、键盘及触控活动的累计时长;
  • 组件 (Component) - DOM 元素的集合,是页面的组成部分。一个页面内可包含多个组件;
  • 组件可见时长 - 一个组件对用户可见的累计时长。

其关系为:

  • 页面活跃时长 ≤ 页面可见时长;
  • 组件可见时长 ≤ 页面可见时长;
  • 一个页面不可见时,则一定不活跃,且其中的所有组件一定也都不可见。

页面可见及活跃时长统计

在我们的设计中,衡量一个页面的停留及活跃时长需要两个重要的指标:

  • 可见性 (visibility)

  • visible - 页面在当前浏览器的 viewport 中,且浏览器窗口未被最小化;

  • invisible - 页面不在当前浏览器的 viewport 中,或因浏览器最小化导致其无法被看到。

  • 活跃性 (activity)

  • active - 用户在网页中有活动(例如鼠标、键盘活动及页面滚动等);

  • inactive - 用户在网页中没有任何活动。

只要能获取到这四种状态发生的时间戳,就可以按下图所示方法,累加计算出页面从载入到退出的可见和活跃时长:

计算页面可见和活跃时长

获取页面可见性数据

Web Lifecycle

Google 公司在 2018 年 7 月份提出了一套用来描述网页生命周期的 Page Lifecycle API 规范,本 SDK 便是基于这套规范来监听页面可见性变化的。规范指出,一个网页从载入到销毁的过程中,会通过浏览器的各种事件在以下六种生命周期状态 (Lifecycle State) 之间相互转化。

生命周期状态 描述
active 网页可见,且具有焦点
passive 网页可见,但处于失焦状态
hidden 网页不可见,但未被浏览器冻结,一般由用户切换到别的 tab 或最小化浏览器触发
frozen 网页被浏览器冻结(一些后台任务例如定时器、fetch 等被挂起以节约 CPU 资源)
terminated 网页被浏览器卸载并从内存中清理。一般用户主动将网页关闭时触发此状态
discarded 网页被浏览器强制清理。一般由系统资源严重不足引起

生命周期状态之间的转化关系如下图所示:

生命周期状态转化

由以上信息,我们可以得出页面生命周期状态和页面可见状态之间的映射关系:

生命周期状态 可见状态
active passive visible
hidden terminated frozen discarded invisible

因此,我们只需要监听页面生命周期的变化并记录其时间,就可以相应获取页面可见性的统计数据。

监听页面生命周期变化

在制定 Page Lifecycle 规范的同时,Google Chrome 团队也开发了 PageLifecycle.js SDK, 实现了这套规范中描述的生命周期状态的监听,并兼容了 IE 9 以上的所有浏览器。出于易用性及稳定性的考虑,我们决定使用这个 SDK 来进行生命周期的监听。由于 PageLifecycle.js 本身使用 JavaScript 编写,我们为其添加了类型定义并封装为了兼容 TypeScript 的 @byted-cg/page-lifecycle-typed SDK. PageLifecycle.js 的使用方法如下:

import lifecycleInstance, {
  StateChangeEvent,
} from "@byted-cg/page-lifecycle-typed";

lifecycleInstance.addEventListener("statechange", (event: StateChangeEvent) => {
  switch (event.newState) {
    case "active":
    case "passive":
      // page visible, do something
      break;
    case "hidden":
    case "terminated":
    case "frozen":
      // page invisible, do something else
      break;
  }
});

通过 PageLifecycle.js, 我们便可以监听 statechange 事件来在页面生命周期发生变化时获得通知,并在生命周期状态为 activepassive 时标记页面为 visible 状态,在生命周期状态为其他几个时标记页面为 invisible 状态,更新最后一次可见的时间戳,并累加页面可见时间。

PageLifecycle.js 的缺陷

对于我们的需求来说,PageLifecycle.js 本身存在如下两个缺陷。我们也针对这两个缺陷做了一些改进。

  • 无法监控到单页应用 (SPA) 中页面的变化

    在单页应用中,页面通常依靠 history 或 hash 路由的变化来切换,页面本身并不会重新载入,因此 PageLifecycle.js 无法感知页面的切换。为了应对这种情况,我们在 monitor-tracer 中手动添加了针对路由变化事件(popstate``replacestate 等)的监听。如果发现页面的路由发生了变化,会认为当前页面进入了 terminated 生命周期,从而进行相应的处理。这里对路由变化进行监听的逻辑复用了我们之前开发的 @byted-cg/puzzle-router SDK, 有兴趣的同学可以参考。

  • 无法捕获 discarded 生命周期

    discarded 生命周期发生在网页被浏览器强制清除时。此时网页已经被销毁并从内存中清理,无法向外传递任何事件,因此 PageLifecycle.js 也就无法推送 discarded 事件。这种情况一旦发生,就会造成被清除的网页统计数据的丢失。为了应对此场景,monitor-tracer 会在页面进入 invisible 状态时,将现有的页面时长统计数据存储使用 JSON.stringify 序列化并储存在 localStorage 当中。如果页面恢复 visible 状态,则会把 localStorage 中的数据清空;而如果页面被清除,则会在下一次进入页面时先将 localStorage 中存储的上一个页面的数据通过事件推送出去。这样就最大程度地保证了页面即使被强制清除,其数据也能被送出而不至丢失。

获取页面活跃性数据

相较于页面可见性,页面活跃性的判断要更加直截了当一些,直接通过以下方法判断页面状态为 active 还是 inactive 即可。

active 判断标准

通过监听一系列的浏览器事件,我们就可以判断用户是否在当前页面上有活动。monitor-tracer 会监听以下的六种事件:

事件 描述
keydown 用户敲击键盘时触发
mousedown 用户点击鼠标按键时触发
mouseover 用户移动鼠标指针时触发
touchstart 用户手指接触触摸屏时触发(仅限触屏设备
touchend 用户手指离开触摸屏时触发(仅限触屏设备)
scroll 用户滚动页面时触发

一旦监听到以上事件,monitor-tracer 就会将页面标记为 active 状态,并记录当前时间戳,累加活跃时长。

inactive 判断标准

以下两种情况下,页面将会被标记为 inactive 状态:

  • 超过一定的时间阈值(默认为 30 秒,可在初始化 SDK 时自定义)没有监测到表示页面活跃的六种事件;
  • 页面状态为 invisible. 因为如果页面对用户不可见,那么它一定是不活跃的。

页面被标记为 inactive 后,monitor-tracer 会记录当前时间戳并累加活跃时长。

组件可见时长统计

统计组件级别活跃时长需要两个条件,一是拿到所有需要统计的 DOM 元素,二是对这些 DOM 元素按照一定的标准进行监控。

获取需要统计的 DOM 元素

监听 DOM 结构变化

获取 DOM 元素要求我们对整个 DOM 结构的改变进行监控。monitor-tracer 使用了 MutationObserver API, DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,都可以通过这个 API 得到通知。

概念上,它很接近事件,可以理解为 DOM 发生变动就会触发 MutationObserver 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件,而 MutationObserver 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。

这样的设计是为了应对 DOM 变动频繁的特点,节省性能。举例来说,如果文档中连续插入 1000 个 `标签,就会连续触发 1000 个插入事件,执行每个事件的回调函数,这很可能造成浏览器的卡顿。而MutationObserver` 完全不同,只在 1000 个标签都插入结束后才会触发,而且只触发一次。

MutationObserver API 的用法如下:

const observer = new MutationObserver(function (mutations, observer) {
  mutations.forEach(function (mutation) {
    console.log(mutation.target); // target: 发生变动的 DOM 节点
  });
});

observer.observe(document.documentElement, {
  childList: true, //子节点的变动(指新增,删除或者更改)
  attributes: true, // 属性的变动
  characterData: true, // 节点内容或节点文本的变动
  subtree: true, // 表示是否将该观察器应用于该节点的所有后代节点
  attributeOldValue: false, // 表示观察 attributes 变动时,是否需要记录变动前的属性值
  characterDataOldValue: false, // 表示观察 characterData 变动时,是否需要记录变动前的值。
  attributeFilter: false, // 表示需要观察的特定属性,比如['class','src']
});

observer.disconnect(); // 用来停止观察。调用该方法后,DOM 再发生变动则不会触发观察器

标记需要监听的元素

为了在众多 DOM 元素中找到需要监听的元素,我们需要一个方法来标记这些元素。monitor-tracer SDK 规定,一个组件如果需要统计活跃时长,则需要为其添加一个 monitor-pvdata-monitor-pv 属性。在使用 MutationObserver 扫描 DOM 变化时,monitor-tracer 会将有这两个属性的 DOM 元素收集到一个数组里,以供监听。例如下面两个组件:

<div monitor-pv='{ "event": "component_one_pv", "params": { ... } }'>
  Component One
</div>
<div>Component Two</div>

Component One 因为添加了 monitor-pv 属性,会被记录并统计可见时长。而 Component Two 则不会。

babel-plugin-tracer 插件

如果需要监控的组件是通过一些组件库(例如 Ant Design 或 ByDesign)编写的,那么为其添加 monitor-pvdata-monitor-pv 这样的自定义属性可能会被组件自身过滤从而不会出现在最终生成的 DOM 元素上,导致组件的监控不生效。为了解决类似的问题,我们开发了 @byted-cg/babel-plugin-tracer babel 插件。此插件会在编译过程中寻找添加了 monitor-pv 属性的组件,并在其外层包裹一个自定义的 <monitor></monitor> 标签。例如:

import { Card } from 'antd';

const Component = () => {
  return <Card monitor-pv={{ event: "component_one_pv", params: { ... } }}>HAHA</Card>
}

如果不添加插件,那么最终生成的 DOM 为:

<div class="ant-card">
  <!-- ... Ant Design Card 组件 -->
</div>

可见 monitor-pv 属性经过组件过滤后消失了。

而安装 babel 插件后,最终编译生成的 DOM 结构为:

<monitor
  is="custom"
  data-monitor-pv='{ "event": "component_one_pv", "params": { ... } }'
>
  <div class="ant-card">
    <!-- ... Ant Design Card 组件 -->
  </div>
</monitor>

monitor-pv 属性得到了保留,并且插件自动为其添加了 data- 前缀,以应对 React 16 之前版本仅支持 data- 开头的自定义属性的问题;同时将传入的对象使用 JSON.stringify 转换成了 DOM 元素 attribute 唯一支持的 string 类型。

由于自定义标签没有任何样式,所以包裹该标签也不会影响到原有组件的样式。monitor-tracer SDK 在扫描 DOM 元素后,会同时收集所有 <monitor></monitor> 标签中的元素的信息,并对其包裹的元素进行监控。

判断 DOM 元素可见性

对组件可见性的判断可分为三个维度:

  • 组件是否在浏览器 viewport 中 - 使用 IntersectionObserver API 判断;
  • 组件样式是否可见 - 根据元素 CSS 的 display``visibilityopacity 样式属性判断;
  • 页面是否可见 - 根据页面可见性判断。

判断组件是否在浏览器 viewport 中

这里我们使用了 IntersectionObserver API. 该 API 提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。

过去,相交检测通常要用到事件监听,并且需要频繁调用 Element.getBoundingClientRect 方法以获取相关元素的边界信息。事件监听和调用 Element.getBoundingClientRect 都是在主线程上运行,因此频繁触发、调用可能会造成性能问题。这种检测方法极其怪异且不优雅。

IntersectionObserver API 会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时(或者 viewport),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理。

IntersectionObserver API 的用法如下:

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(function (entry) {
      /**
     entry.boundingClientRect // 目标元素的矩形区域的信息
     entry.intersectionRatio // 目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为 1,完全不可见时小于等于 0
     entry.intersectionRect // 目标元素与视口(或根元素)的交叉区域的信息
     entry.isIntersecting // 标示元素是已转换为相交状态 (true) 还是已脱离相交状态 (false)
     entry.rootBounds // 根元素的矩形区域的信息, getBoundingClientRect 方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回 null
     entry.target // 被观察的目标元素,是一个 DOM 节点对象
     entry.time // 可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
     **/
    });
  },
  {
    threshold: [0, 0.25, 0.5, 0.75, 1], //该属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为 [0],即交叉比例 (intersectionRatio) 达到 0 时触发回调函数
  }
);

observer.observe(document.getElementById("img")); // 开始监听一个目标元素

observer.disconnect(); // 停止全部监听工作

如果一个组件和 viewport 的相交比例小于某个值(默认为 0.25),那么这个组件就会被标记为 invisible. 反之,如果比例大于某个值(默认为 0.75),那么这个组件就会被标记为 visible

判断组件 CSS 样式是否可见

如果元素的 CSS 样式设为了 visibility: hiddenopacity: 0,那么即使其与 viewport 的相交比例为 1,对用户来说也是不可见的。因此我们需要额外判断目标元素的 CSS 属性是否为可见。

如果一个组件的样式被设置为了以下之一,那么它就会被标记为 invisible.

  • visibility: hidden
  • display: none
  • opacity: 0

判断页面是否可见

当页面不可见时,所有组件自然都不可见,因此在页面为 invisible 的状态下,monitor-tracer 会将需监控的所有组件的状态也标记为 invisible

处理「触发事件」的 SDK - monitor

monitor SDK 的定位

数据平台 SDK 的单一的埋点上报方式,无法满足我们开发中对 clean code 的极致追求

数据平台的 SDK 往往只提供了上报埋点的函数式方法,虽然可以满足我们的日常开发需求,但是并不能解决我们在写埋点代码时的两大痛点:

  • 只能逐个进行埋点上报
  • 埋点逻辑与业务逻辑的耦合

我们希望埋点代码可以轻易地添加、修改与删除,并且对业务代码没有影响。因此,我们基于 TypeScript 开发对框架无感的 monitor SDK. 它支持逐个上传多个埋点,并且接受返回埋点的函数,将其返回值上报;它提供了三种方式注入埋点,覆盖了所有场景,将埋点与业务代码完全分离。

可以看出,monitor 既是一个数据处理器,又是一个方法库。更直观一些,使用 monitor 后,我们的应用在上报埋点时的流程如下:

埋点上报流程

埋点由应用层发送给 monitor 后,monitor 首先会对数据进行处理,再调用数据平台 SDK, 将埋点事件上报给数据平台。

在对 monitor 有了初步了解后,这篇文章将主要讲解 monitor 是如何通过以下三种埋点注入的方式,解耦业务逻辑与埋点逻辑的。

下面我们来看一下 monitormonitor-tracer SDK 具体的技术设计及实现方法。

三种埋点注入方式

类指令式

monitor 提供了类指令方式注入埋点。例如,下段代码用 monitor-click 指令注入了埋点。在此按钮被点击 (click) 时,monitor-click 所对应的值,即一个事件,就会被上报。

// 指令式埋点示例
<Button
  monitor-click={JSON.stringify({
    type: "func_operation",
    params: { value: 3 },
  })}
>
  Click Me
</Button>

这是如何实现的呢?为什么仅仅给组件加了一个 monitor-click 属性,monitor 就会在这个按钮被点击时上报埋点了呢?

实现与原理

其实,monitor SDK 在初始化时,会给当前的 document 对象加上一系列 Event Listeners, 监听 hover``click``input``focus 等事件。当监听器被触发时,monitor 会从触发事件的 target 对象开始,逐级向上遍历,查看当前元素是否有对应此事件的指令,如果有,则上报此事件,直至遇到一个没有事件指令的元素节点。以下示意图展示了类指令式埋点的上报流程:

类指令上报流程

逐级上报过程

以如下代码为例,当光标 hover 到 Button 时,document 对象上所安装的监听 hover 事件的函数便会执行。这个函数首先在 event.targetButton 上查找是否有与 hover 事件相关的指令(即属性)。Buttonmonitor-hover 这个指令,此时函数便上传此指令所对应的事件,即 { type: 'func_operation', params: { value: 1 }}

接下来,函数向上一层,到了 Button 的父元素,即 div, 重复上述过程,它找到了 data-monitor-hover 这个指令,便同样地上报了对应的埋点事件。而到了 section 这一层,虽然其有 data-monitor-click 指令,但此指令并不对 hover 事件进行响应,因此,这个逐级上报埋点的过程结束了。

// 指令式埋点实现逐级上报
<section
        data-monitor-click={JSON.stringify({
        type: 'func_operation',
params: { value: 3 },
})}
>
<div
        data-monitor-hover={JSON.stringify({
        type: 'func_operation',
params: { value: 2 },
})}
>
<Button
        monitor-hover={JSON.stringify({
        type: 'func_operation',
params: { value: 1 },
})}
>
Click Me
</Button>
</div>
</section>

类指令式埋点注入适合简单的埋点上报,清晰的与业务代码实现了分离。但是如果我们需要在上报事件前,对所上报的数据进行处理,那么这种方式就无法满足了。并且,并不是所有的场景都可以被 DOM 事件所覆盖。如果我想在用户在搜索框输入某个值时,上报埋点,那么我就需要对用户输入的值进行分析,而不能在 input 事件每次触发时都上报埋点。

装饰器式

装饰器本质上是一个高阶函数。它接受一个函数,返回另一个被修饰的函数。因此,我们很自然地想到用装饰器将埋点逻辑注入到业务函数,既实现了埋点与业务代码的分离,又能够适应于复杂的埋点场景。

下面的代码使用了 @monitorBefore 修饰器。@monitorBefore 所接收的函数的返回值,即是要上报的事件。在 handleSearch 函数被调用的时候,monitor 会首先上报埋点事件,然后再执行 handleSearch 函数的逻辑。

// @monitorBefore 使用示例
@monitorBefore((value: string) => ({
    type: 'func_operation',
    params: { keyword: value },
}))
handleSearch() {
    console.log(
        '[Decorators Demo]: this should happen AFTER a monitor event is sent.',
    );
}

return (
    <AutoComplete
        onSearch={handleSearch}
/>
)

@readonly 理解装饰器原理

装饰器是如何实现将埋点逻辑和业务逻辑相整合的呢?在我们详细解读 @monitorBefore 之前,让我们先从一个常用的装饰器 @readonly讲起吧。

装饰器应用于一个类的单个成员上,包括类的属性、方法、getters 和 setters. 在被调用时,装饰器函数会接收 3 个参数:

  • target - 装饰器所在的类
  • name - 被装饰的函数的名字
  • descriptor - 被装饰的函数的属性描述符
// @readonly装饰器的代码实现
readonly = (target, name, descriptor) => {
  console.log(descriptor);
  descriptor.writable = false;
  return descriptor;
};

上述代码通过 console.log 输出的结果为:

代码输出结果 以我们常见的 @readonly 为例,它的实现方法如上。通过在上述代码中 log 出来 descriptor, 我们得知 descriptor 的属性分别为:

  • writable - 被装饰的函数是否能被赋值运算符改变;
  • enumerable - 被装饰的函数是否出现在对象的枚举属性中;
  • configurable - 被装饰的函数的描述符是否能够被改变,是否能够从对象上被删除;
  • value - 被装饰的函数的值,即其对应的函数对象。

可见,@readonly 装饰器将 descriptorwritable 属性设置为 false, 并返回这个 descriptor, 便成功将其装饰的类成员设置为只读态。

我们以如下方式使用 @readonly 装饰器:

class Example {
  @readonly
  a = 10;

  @readonly
  b() {}
}

@monitorBefore 的实现

@monitorBefore 装饰器要比 @readonly 复杂一些,它是如何将埋点逻辑与业务逻辑融合,生成一个新的函数的呢?

首先,我们来看看下面这段代码。monitorBefore 接收一个埋点事件 event 作为参数,并返回了一个函数。返回的函数的参数与上面讲过的 @readonly 所接收的参数一致。

// monitorBefore 函数源代码
monitorBefore = (event: MonitorEvent) => {
  return (target: object, name: string, descriptor: object) =>
    this.defineDecorator(event, descriptor, this.before);
};
// @monitorBefore 使用方式
@monitorBefore((value: string) => ({
    type: 'func_operation',
    params: { keyword: value },
}))
handleSearch() {
    console.log(
        '[Decorators Demo]: this should happen AFTER a monitor event is sent.',
    );
}

return (
    <AutoComplete
        onSearch={handleSearch}
/>
)

在编译时,@monitorBefore 接收了一个 event 参数,并返回了如下函数:

f = (target: object, name: string, descriptor: object) =>
  this.defineDecorator(event, descriptor, this.before);

而后,编译器又调用函数 f, 把当前的类、被装饰的函数名称与其属性描述符传给f. 函数 f 返回的 this.defineDecorator(event, descriptor, this.before) 会被解析为一个新的 descriptor 对象,它的 value 会在运行时被调用,也就是说会在 onSearch 被触发时所调用。

现在,让我们详细解读 defineDecorator 函数是如何改变生成一个新的 descriptor 的吧。

// defineDecorator 函数源代码
before = (event: MonitorEvent, fn: () => any) => {
  const that = this;
  return function (this: any) {
    const _event = that.evalEvent(event)(...arguments);
    that.sendEvent(_event);
    return fn.apply(this, arguments);
  };
};

defineDecorator = (
  event: MonitorEvent,
  descriptor: any,
  decorator: (event: MonitorEvent, fn: () => any) => any
) => {
  if (isFunction(event) || isObject(event) || isArray(event)) {
    const wrapperFn = decorator(event, descriptor.value);

    function composedFn(this: any) {
      return wrapperFn.apply(this, arguments);
    }

    set(descriptor, "value", composedFn);
    return descriptor;
  } else {
    console.error(
      `[Monitor SDK @${decorator}] the event argument be an object, an array or a function.`
    );
  }
};

monitorBefore = (event: MonitorEvent) => {
  return (target: object, name: string, descriptor: object) =>
    this.defineDecorator(event, descriptor, this.before);
};

defineDecorator 函数接收三个参数:

  • event - 需要上报的埋点;
  • descriptor - 被装饰的函数的属性描述符;
  • decorator - 一个高阶函数。它接收一个埋点事件与一个回调,返回一个函数,用来进行埋点上报,并而后执行回调。

decorator 首先返回了一个函数 wrapperFn. 在被调用时,wrapperFn 会先上报埋点,然后执行 descriptor.value 的逻辑,即被装饰的函数。

defineDecoratorcomposedFn 中, 我们用 wrapperFn.apply(this, arguments) 将调用被装饰的函数时传入的参数透传给 wrapperFn

最后,我们将 composedFn 设为 descriptor.value, 这样,我们就成功生成了一个融合了埋点逻辑与业务逻辑的新函数。

装饰器埋点注入方式十分整洁,能够清晰地与业务代码区分。不论是增添、修改还是删除埋点,都无需顾虑会对业务代码造成改动。但是其局限性也是显而易见的,装饰器只能用于类组件,现在我们常用的函数式组件是无法使用装饰器的。

React 钩子

为了能够在函数式组件中,实现装饰器埋点带来的功能,我们还支持了埋点钩子 useMonitor. 与装饰器的原理相同,useMonitor 接收一个埋点函数,一个业务函数,返回一个新的函数将二者融合,既实现了代码层面上的清晰分离,又覆盖了全场景的埋点注入。

// useMonitor 源代码
useMonitor = (fn: () => any, event: MonitorEvent) => {
  if (!event) return fn;
  const that = this;

  return function (this: any) {
    const _event = that.evalEvent(event)(...arguments);
    that.sendEvent(_event);

    return fn.apply(this, arguments);
  };
};

useMonitor 的实现较为简单,只是一个高阶函数,不像装饰器需要语法解析。它返回了一个函数,在被调用时会先上传埋点事件,在执行业务逻辑。其使用方式如下:

// useMonitor 使用示例
const Example = (props: object) => {
  const handleChange = useMonitor(
    // 业务逻辑
    (value: string) => {
      console.log("The user entered", value);
    },
    // 埋点逻辑
    (value: string) => {
      return {
        type: "func_operation",
        params: { value },
      };
    }
  );

  return <Search onSearch={handleChange} />;
};

小结

上述三种埋点方式,覆盖了所有使用场景。不论你是用 React, Vue, 还是原生 JavaScript, 不论你是使用类组件,还是函数式组件,不论你的埋点是否需要复杂的前置逻辑,monitor SDK 都提供了适合你的场景的使用方式。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/rvJDfV0fer4jHlDKHC4-qw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录