Published on

Simple Tracker

Authors
  • avatar
    Name
    Jack Fan

Tracker

Init

输入以下命令初始化一个项目。


npm init

tsc --init

npm install rollup -D
npm install rollup-plugin-dts -D
npm install rollup-plugin-typescript2 -D
npm install typescript -D

版本号为

{
  "rollup": "^2.77.0",
  "rollup-plugin-dts": "^4.2.2",
  "rollup-plugin-typescript2": "^0.32.1",
  "typescript": "^4.9.5"
}

新建基础目录结构

src/core/index.ts---核心代码

src/types/index.ts---工具函数

score/utils/pv.ts---声明文件

新建 rollup.config.js进行 rollup 配置。

// rollup.config.js

import ts from "rollup-plugin-typescript2";
import path from "path";
import dts from "rollup-plugin-dts";
export default [
  {
    //入口文件
    input: "./src/core/index.ts",
    output: [
      //打包esModule
      {
        file: path.resolve(__dirname, "./dist/index.esm.js"),
        format: "es",
      },
      //打包common js
      {
        file: path.resolve(__dirname, "./dist/index.cjs.js"),
        format: "cjs",
      },
      //打包 AMD CMD UMD
      {
        input: "./src/core/index.ts",
        file: path.resolve(__dirname, "./dist/index.js"),
        format: "umd",
        name: "tracker",
      },
    ],
    //配置ts
    plugins: [ts()],
  },
  {
    //打包声明文件
    input: "./src/core/index.ts",
    output: {
      file: path.resolve(__dirname, "./dist/index.d.ts"),
      format: "es",
    },
    plugins: [dts()],
  },
];

增加打包命令

{
  "scripts": {
    "build": "rollup -c"
  }
}

Init Tracker

// core/index.ts

export default class Tracker {
  constructor(options) {}

  private initDef() {}
}

初始化一个Tracker类,constructor接受一些 options,接下来在types/index.ts定义一些类型。

// core/index.ts

/**
 * @requestUrl 接口地址
 * @historyTracker history上报
 * @hashTracker hash上报
 * @domTracker 携带Tracker-key 点击事件上报
 * @sdkVersionsdk版本
 * @extra透传字段
 * @jsError js 和 promise 报错异常上报
 */
export interface DefaultOptons {
  uuid: string | undefined;
  requestUrl: string | undefined;
  historyTracker: boolean;
  hashTracker: boolean;
  domTracker: boolean;
  sdkVersion: string | number;
  extra: Record<string, any> | undefined;
  jsError: boolean;
}

//必传参数 requestUrl
export interface Options extends Partial<DefaultOptons> {
  requestUrl: string;
}

//版本
export enum TrackerConfig {
  version = "1.0.0",
}

首先此处DefaultOptions定义了options的属性,将其使用在initDef上,让其初始化配置

export default class Tracker {
  // ...
  private initDef(): DefaultOptons {
    return <DefaultOptons>{
      sdkVersion: TrackerConfig.version,
      historyTracker: false,
      hashTracker: false,
      domTracker: false,
      jsError: false,
    };
  }
}

其次Options则为constructor内参数的类型,此处使用Partial去扩展DefaultOptions,使其内所有参数都变得可选,而requestUrl是必写参数,因此再把他加上,同时定义一个TrackerConfig储存 version。

最后,在类里面定义一个data变量,储存初始化后得到的options

import { DefaultOptons, Options, TrackerConfig } from "../types/index";

export default class Tracker {
  public data: Options;
  constructor(options: Options) {
    this.data = Object.assign(this.initDef(), options);
  }

  private initDef(): DefaultOptons {
    return <DefaultOptons>{
      sdkVersion: TrackerConfig.version,
      historyTracker: false,
      hashTracker: false,
      domTracker: false,
      jsError: false,
    };
  }
}

PV(Page View)

PV 页面访问量,即 PageView,用户每次对网站的访问均被记录

主要监听了 history 和 hash

history 的 API 包括:go backforwardpushStatereplaceState

hash 可以使用 hashchange 监听。

但是对于 history,popState可以监听浏览器的前进后退操作(历史记录的上一页和下一页),和 history 方法的goback方法,对于pushStatereplaceState 无法有效监听,因此我们在 utils/pv.ts重写方法。

// utils/pv.ts

export const createHistoryEvent = <T extends keyof History>(type: T) => {
  const origin = history[type];

  return function (this: any) {
    const res = origin.apply(this, arguments);

    const e = new Event(type);

    window.dispatchEvent(e);

    return res;
  };
};

Event 创建自定义事件

dispatchEvent 派发事件

addEventListener 监听事件

removeEventListener 删除事件

其实也就是 发布订阅模式

这里首先先获取原先 history 里面对应的方法(例如go方法),获取到以后返回一个高阶函数,里面调用origin方法并获取结果res并返回。然后在里面创建自定义事件然后将它 dispatch 出去。

再回到core/index.ts调用它

// core/index.ts

export default class Tracker {
  // ...
  private initDef(): DefaultOptons {
    window.history["pushState"] = createHistoryEvent("pushState");
    window.history["replaceState"] = createHistoryEvent("replaceState");
  }
  // ...
}

这样,我们就修改了原来 history 上的方法,使其变的可以被监听。

接下来可以根据用户的选项,来进行调用决定是否监听 history 和 hash。

export default class Tracker {
  // ...
  private captureEvent<T>(
    mouseEventList: string[],
    targetKey: string,
    data?: T
  ) {
    mouseEventList.forEach((event) =>
      window.addEventListener(event, () => {
        console.log("Tracking!");
      })
    );
  }

  private installTracker() {
    if (this.data.historyTracker)
      this.captureEvent(
        ["pushState", "replaceState", "popstate"],
        "history-pv"
      );

    if (this.data.hashTracker) this.captureEvent(["hashchange"], "hash-pv");
  }
}

在这里的第二个参数targetKey,通常是与后端协商的。例如,触发监听后,需要给回一些东西。

新建一个index.html,引入打包好的 js 文件,并启用 tracker。

<script src="./dist/index.js"></script>
<script>
  new tracker({
    historyTracker: true,
  });
</script>

通过 live server 打开 html 文件,在控制台输入如 history.pushState("123", "", "/a"),会看到控制台输出Tracking!。表面已经成功监听,若此时点击返回上一页按钮,控制台也会输出语句。

UV(Unique Visitor)

Unique Visitor,访问网站的一台电脑客户端为一个访客。 用户唯一标识:可以在登陆过后生成一个 UUId 后储存在 localStorage 里面,然后上报。也可以使用 canvas 指纹追踪技术。 登录之后,后台会返回一个 id,所以我们暴露一个setUserId的方法,用于设置并储存 id。

export default class Tracker {
  public setUserId<T extends DefaultOptons["uuid"]>(uuid: T) {
    this.data.uuid = uuid;
  }

  public setExtra<T extends DefaultOptons["extra"]>(extra: T) {
    this.data.extra = extra;
  }
}

暴露两个方法,用于设置 id,和用户的一些自定义选项 extra。

reportTracker

现在来使用 navigator.sendBeacon 进行上报。

为什么不适用 XMLHttpRequest?因为页面关闭后,navigator.sendBeacon仍会进行上传,XMLHttpRequest 不一定会

export default class Tracker {
  // ....
  private reportTracker<T>(data: T) {
    const params = Object.assign(this.data, data, {
      time: new Date().getTime(),
    });
    let headers = { type: "application/x-www-form-urlencoded" };
    let blob = new Blob([JSON.stringify(params)], headers);
    navigator.sendBeacon(this.data.requestUrl, blob);
  }
}

reportTracker接受 data,然后与 this.data 和 现在的时间组成一个新的 object params。设置好 headers 后生成一个 blob,用作navigator.sendBeacon的第二个参数。

export default class Tracker {
  // ....
  public sendTracker<T>(data: T) {
    this.reportTracker(data);
  }

  private captureEvent<T>(
    mouseEventList: string[],
    targetKey: string,
    data?: T
  ) {
    mouseEventList.forEach((event) =>
      window.addEventListener(event, () => {
        this.reportTracker({ event, targetKey, data });
      })
    );
  }
  //...
}

然后先新增一个方法sendTracker用于手动上报,其次则将captureEvent修改,使其完成自动上报。

Write an API to test our Tracker

现在来写个 API 测试一下 Tracker 是否有效果

// index.html
new tracker({
  requestUrl: "http://localhost:9000/tracker",
  historyTracker: true,
});

第一步,先在index.html补全requestUrl。 第二步,建立一个 express 服务器。

// server/index.js
const express = require("express");
const cors = require("cors");

const app = express();

app.use(cors());

app.use(express.urlencoded({ extended: false }));

app.post("/tracker", (req, res) => {
  console.log(req.body);
  res.send(200);
});

app.listen(9000, () => console.log("Successfully listen on 9000."));

输入node index.js运行服务器后,使用 Live Server 跑起 index.html。再到控制台,输入history.pushState('adsd','','/a')。 这个时候到 Network 网络部分,可以看到 tracker 以 ping 的形式发送请求。

Network-Trakcer-ping

点到载荷也可以看到表单数据:

{
  "sdkVersion": "1.0.0",
  "historyTracker": true,
  "hashTracker": false,
  "domTracker": false,
  "jsError": false,
  "requestUrl": "http://localhost:9000/tracker",
  "event": "pushState",
  "targetKey": "history-pv",
  "time": 1683125100046
}

来到 Express 后台,可以看到上报的数据,evnet 为pushState,如果点一下浏览器的返回按钮,会发现也上报了,event 为popstate

Express-backend

DOM Event Tracker

DOM 事件监听。主要是给需要监听的元素添加一个属性target-key用来区分是否需要监听。

<button target-key="btn">Button</button> <button>No Tracker</button>

首先,先准备好要监听的事件列表

const MouseEventList: string[] = [
  "click",
  "dblclick",
  "contextmenu",
  "mousedown",
  "mouseup",
  "mouseenter",
  "mouseout",
  "mouseover",
];

然后,遍历事件列表,做全局监听,如果发现元素有target-key属性,就进行上报

export default class Tracker {
  // ...
  private targetKeyReport() {
    MouseEventList.forEach((ev) =>
      window.addEventListener(ev, (e) => {
        const target = e.target as HTMLElement;
        const targetKey = target.getAttribute("target-key");
        if (targetKey) this.reportTracker({ event: ev, targetKey });
      })
    );
  }
  // ...
}

最后,再将targetKeyReport添加到installTracker

if (this.data.domTracker) this.targetKeyReport();

JavaScript Error Tracker

对于 JavaScript 的报错,普通错误会触发error事件,promise错误则会触发unhandledrejection事件。

export default class Tracker {
  // ...
  private jsError() {
    this.errorEvent();
    this.promiseReject();
  }

  private errorEvent() {
    window.addEventListener("error", (event) =>
      this.reportTracker({
        event: "error",
        targetKey: "message",
        message: event.message,
      })
    );
  }

  private promiseReject() {
    window.addEventListener("unhandledrejection", (event) =>
      event.promise.catch((error) =>
        this.reportTracker({
          event: "promiseReject",
          targetKey: "message",
          message: error,
        })
      )
    );
  }
  // ...
}

jsError添加到installTracker

if (this.data.jsError) this.jsError();