前端实现埋点监控

news2024/12/28 5:30:58

前端实现埋点&监控

实现埋点功能的意义主要体现在以下几个方面:

  1. 数据采集:埋点是数据采集领域(尤其是用户行为数据采集领域)的术语,它针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。通过埋点,可以收集到用户在应用中的所有行为数据,例如页面浏览、按钮点击、表单提交等。
  2. 数据分析:采集的数据可以帮助业务人员分析网站或者App的使用情况、用户行为习惯等,是后续建立用户画像、用户行为路径等数据产品的基础。通过数据分析,企业可以更好地了解用户需求,优化产品和服务。
  3. 改进决策:通过对埋点数据的分析,企业可以了解用户的真实需求和行为习惯,从而做出更符合市场和用户需求的决策,提高产品和服务的质量和竞争力。
  4. 优化运营:通过埋点数据,企业可以了解用户的兴趣和行为,从而更好地定位目标用户群体,优化运营策略,提高运营效率和收益。
  5. 预测趋势:通过对埋点数据的分析,企业可以预测市场和用户的未来趋势,从而提前做好准备,把握市场机遇,赢得竞争优势。

总之,实现埋点功能可以帮助企业更好地了解用户需求和行为习惯,优化产品和服务,改进决策,优化运营并预测趋势,具有重要的意义和作用。

常见的埋点包括:pv【PageView】上报(包括history上报、hash上报)、uv【UserView】上报、dom事件上报、js报错上报(包括常规错误上报、Promise报错上报)

下面我们通过使用nodejsTypeScriptrollup等技术栈实现一个简易的埋点上报的sdk,并发布npm。

通过这篇文章你可以学习到:埋点&监控、区分js模块化、打包工具rollup、API之History、JS二进制、sendBeacon发送post请求等知识。

一、前置知识

1. 区分JS模块化

因为要在nodejs环境下并使用rollup打包输出支持不同规范的模块,因此了解JS模块化相关知识是必要的。

主流模块化规范有:

  • CommonJS规范

  • AMD规范

  • CMD规范

  • ESM规范

  • UMD规范

这里建议看一下这篇博客:前端模块化详解(完整版),里面详细讲解了主流模块

下面我们对主流模块做个总结,如下:

序号模块化规范备注
1CommonJSCommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMDCMD解决方案
2AMDAMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
3CMD CMD规范整合了CommonJSAMD规范的特点, CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
4UMDUMDAMDCommonJS两者的结合,这个模式中加入了当前存在哪种规范的判断,所以能够“通用”,它兼容了AMDCommonJS,同时还支持老式的“全局”变量规范
5ESMES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

2. rollup

要发布npm包,打包库是必然的,而rollupwebpack更适合打包库,因此学习rollup也是必要的。

建议看一下这篇博客:安装 Rollup 以及 Rollup 和 Webpack 的区别

这里总结一下rollupwebpack的区别:

  1. webpack 由于年代相对久远,在 commonjs 后且 esMoudles 之前,所以通过 webpack 通过自己来实现 commonjs 等语法,rollup 则可以通过配置打包成想要的语法,比如 esm
  2. 所以说 rollup 很适合打包成 ,而 webpack 比较适合用来做来打包应用
  3. 由于rollup不能够直接读取node_modules中的依赖项,需要引入加载npm模块的插件:rollup-plugin-node-resolve
  4. 由于rollup默认只支持esm模块打包,所以需要引入插件来支持cjs模块:rollup-plugin-commonjs
  5. 由于 rollup 通过可以 esm 模块开发和打包,所以支持 tree-shaking 模式
  6. vite 就是 rollup 开发而来的

3. History

实现Page View埋点往往需要使用HistoryAPI,因为它可以帮助我们更好地控制页面的状态和导航。

在SPA中,页面的状态通常由内部状态管理,而不是通过URL来表现。因此,传统的PV埋点方法(例如通过document.referrer)可能无法正确计算PV。

使用History API可以让我们更精细地控制页面的导航和状态。我们可以使用history.pushState()方法将新的状态添加到历史记录中,并更新URL,但不会触发页面刷新。这样,我们可以在用户与页面交互时跟踪其导航路径,并计算PV。

另外,当用户点击浏览器的后退按钮时,我们可以使用popstate事件来获取上一个历史记录状态,并根据需要进行处理。这可以帮助我们处理用户在SPA中的导航,并提供更准确的PV数据。

History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录,它的实例方法back()forward()go()大家应该都比较熟悉就不一一介绍了,这里主要介绍一下pushState()replaceState()以及popstate事件,注意:popstate事件并么有使用驼峰命名方式。

3.1 history.pushState()

在HTML文档中,history.pushState()方法向浏览器的会话历史栈增加了一个条目。该方法是异步的。为 popstate 事件增加监听器,以确定导航何时完成。state 参数将在其中可用。

语法:history.pushState(state, title, url)

其中,state 对象是一个 JavaScript 对象,其与通过 pushState() 创建的新历史条目相关联。每当用户导航到新的 state,都会触发 popstate 事件,并且该事件的 state 属性包含历史条目 state 对象的副本。

title标题,由于历史原因是个必填项。url可以是相对路径也可以是绝对路径,浏览器会跳转到对应页面。

对比window.loacation

从某种程度来说,调用 pushState() 类似于 window.location = "#foo"window.location.hash将变成#foohash一般情况下为url后#及其后面一部分组成,这一部分为网页的位置,也称为为锚点),它们都会在当前的文档中创建和激活一个新的历史条目。但是 pushState() 有以下优势:

  • 新的 URL 可以是任何和当前 URL 同源的 URL。然而,如果你仅修改 hash,将其设置到 window.location,将使你留在同一文档中。【pushState即使是和当前同源的url也能加一条历史到历史栈】
  • 改变页面的 URL 是可选的。相反,设置 window.location = "#foo"; 仅仅会在当前 hash 不是 #foo 情况下,创建一条新的历史条目。
  • 你可以使用你的新历史条目关联任意数据。使用基于 hash 的方式,你需要将所有相关的数据编码为一个短字符串。

更详细内容可以看MDN——pushState()方法

3.2 history.replaceState()

replaceState()方法使用state objects, title,和 URL 作为参数,修改当前历史记录实体,如果你想更新当前的 state 对象或者当前历史实体的 URL 来响应用户的的动作的话这个方法将会非常有用。

语法:history.replaceState(stateObj, title[, url])

更详细内容可以看MDN——replaceState()方法

3.3 popstate事件

每当激活同一文档中不同的历史记录条目时,popstate 事件就会在对应的 window 对象上触发。如果当前处于激活状态的历史记录条目是由 history.pushState() 方法创建的或者是由 history.replaceState() 方法修改的,则 popstate 事件的 state 属性包含了这个历史记录条目的 state 对象的一个拷贝。

调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。

更详细内容可以看MDN——popstate事件

4. JS二进制

JavaScript 提供了一些 API 来处理文件或原始文件数据,例如:FileBlobFileReaderArrayBufferbase64 等。它们之间的关系如下:

img

以下是我对谈谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64这篇文章的一点小结,详细内容请看原文,建议把原文的代码都敲一遍

4.1 Blob

Blob(binary large object),即二进制大对象,它是 JavaScript 中的一个对象,表示原始的类似文件的数据。

Blob对象是一个只读不可修改的二进制文件,它的数据可以按文本或二进制的格式进行读取。

创建Blob对象

new Blob(array, option)

其中

  • array:由 ArrayBufferArrayBufferViewBlobDOMString 等对象构成的,将会被放进 Blob
  • options:可选的 BlobPropertyBag 字典,它可能会指定如下两个属性。
    • type:默认值为 “”,表示放入到blob中的数组内容的MIME类型。
    • endings:默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入,不常用。

常见MIME类型如下:

img

示例:以下实现了实例化了一个Blob对象并使用URL.createObjectUrl()方法将其转化为一个URL。

在这里插入图片描述

点击这个url可以看到如下结果:

在这里插入图片描述

4.2 File

文件(File)接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。实际上,File 对象是特殊类型的 Blob,且可以用在任意的 Blob 类型的 context 中。Blob 的属性和方法都可以用于 File 对象。

注意:File 对象中只存在于浏览器环境中,在 Node.js 环境中不存在。

在 JavaScript 中,主要有两种方法来获取 File 对象:

  • <input> 元素上选择文件后返回的 FileList 对象
  • 文件拖放操作生成的 DataTransfer 对象;

下面实现一下拖放文件生成dataTransfer对象并输出结果:

<div style="width: 500px;height: 500px;background-color: gray;" id="dorp-zone"></div>
<script>
    const dorpZone = document.getElementById('dorp-zone');
    dorpZone.addEventListener('drop', (e) => {
        // 阻止默认事件 如放置文件将显示在浏览器新建窗口中
        e.preventDefault();
        console.log(e);
        const file = e.dataTransfer.files[0];
        console.log(file);
    })
    dorpZone.addEventListener('dragover', (e) => {
        e.preventDefault();
    })
</script>

结果如下:

在这里插入图片描述

4.3 FileReader

FileReader 是一个异步 API,用于读取文件并提取其内容以供进一步使用。FileReader 可以将 Blob 读取为不同的格式。

注意:FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,不能用于从文件系统中按路径名简单地读取文件

创建FIleReader对象

const reader = new FileReader()

FileReader对象常用的属性如下:

  • error:表示在读取文件时发生的错误

  • result:文件内容。该属性仅在读取操作完成后才有效,数据的格式取决于使用哪个方法来启动读取操作。

  • readyState:表示FileReader状态的数字。取值如下:

    常量名描述
    EMPTY0还没有加载任何数据
    LOADING1数据正在被加载
    DONE2已完成全部的读取请求

FileReader对象提供了以下方法来加载文件:

  • readAsArrayBuffer():读取指定 Blob 中的内容,完成之后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象
  • readAsBinaryString():读取指定 Blob 中的内容,完成之后,result 属性中将包含所读取文件的原始二进制数据
  • readAsDataURL():读取指定 Blob 中的内容,完成之后,result 属性中将包含一个data: URL 格式的 Base64 字符串以表示所读取文件的内容
  • readAsText():读取指定 Blob 中的内容,完成之后,result 属性中将包含一个字符串以表示所读取的文件内容

FileReader对象常用事件如下:

  • abort:该事件在读取操作被中断时触发
  • error:该事件在读取操作发生错误时触发
  • load:该事件在读取操作完成时触发
  • progress:该事件在读取 Blob 时触发

示例:以下实现了上传图片并转成base64的url以展示在页面

<input type="file" id="fileInput">
<img id="img" src="" alt="">
<script>
    const fileInput = document.getElementById('fileInput');
    const img = document.getElementById('img');
    const reader = new FileReader();
    fileInput.addEventListener('change', (e) => {
        console.log(e);
        const file = e.target.files[0];
        // reader.readAsText(file);  // 把图片文件转成字符串会是一大堆乱码
        reader.readAsDataURL(file);  // 转成base64的url
        reader.onload = (e) => {
            img.src = e.target.result;
            console.log(e.target.result);
        }
    })
</script>

结果如下:

在这里插入图片描述

4.4 ArrayBuffer

ArrayBuffer对象用来表示通用的、固定长度的原始二进制数据缓冲区ArrayBuffer 的内容不能直接操作,只能通过 DataView 对象或 TypedArrray 对象来访问。这些对象用于读取和写入缓冲区内容。

ArrayBuffer 本身就是一个黑盒,不能直接读写所存储的数据,需要借助以下视图对象来读写:

  • TypedArray:用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图。
  • DataViews:用来生成内存的视图,可以自定义格式和字节序。
4.5 Object URL

Object URL(MDN定义名称)又称Blob URL(W3C定义名称),是HTML5中的新标准。它是一个用来表示File ObjectBlob Object 的URL。在网页中,我们可能会看到过这种形式的 Blob URL

blob:https://zhuanlan.zhihu.com/47cca259-d9cd-41dc-b2a9-319c1db26f32

其实 Blob URL/Object URL 是一种伪协议,允许将 BlobFile 对象用作图像、二进制数据下载链接等的 URL 源

对于 Blob/File 对象,可以使用 URL构造函数的 createObjectURL() 方法创建将给出的对象的 URL。这个 URL 对象表示指定的 File 对象或 Blob 对象。我们可以在<img><script> 标签中或者 <a><link> 标签的 href 属性中使用这个 URL。

4.6 Base64

Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。Base64 编码普遍应用于需要通过被设计为处理文本数据的媒介上储存和传输二进制数据而需要编码该二进制数据的场景。这样是为了保证数据的完整并且不用在传输过程中修改这些数据

JavaScript 中,有两个函数被分别用来处理解码和编码 base64 字符串:

  • atob():解码,解码一个 Base64 字符串;
  • btoa():编码,从一个字符串或者二进制数据编码一个 Base64 字符串。
btoa("JavaScript")       // 'SmF2YVNjcmlwdA=='
atob('SmF2YVNjcmlwdA==') // 'JavaScript'

应用场景如:使用toDataURL()方法把 canvas 画布内容生成 base64 编码格式的图片:

const canvas = document.getElementById('canvas'); 
const ctx = canvas.getContext("2d");
const dataUrl = canvas.toDataURL();

除此之外,还可以使用readAsDataURL()方法把上传的文件转为base64格式的data URI,可看4.3节的实例。

4.7 格式转换

看完这些基本的概念,下面就来看看常用格式之间是如何转换的。

(1)ArrayBuffer → blob

const blob = new Blob([new Uint8Array(buffer, byteOffset, length)]);

(2)ArrayBuffer → base64

const base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));

(3)base64 → blob

const base64toBlob = (base64Data, contentType, sliceSize) => {
  const byteCharacters = atob(base64Data);
  const byteArrays = [];

  for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    const slice = byteCharacters.slice(offset, offset + sliceSize);

    const byteNumbers = new Array(slice.length);
    for (let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);
    byteArrays.push(byteArray);
  }

  const blob = new Blob(byteArrays, {type: contentType});
  return blob;
}

(4)blob → ArrayBuffer

function blobToArrayBuffer(blob) { 
  return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject;
      reader.readAsArrayBuffer(blob);
  });
}

(5)blob → base64

function blobToBase64(blob) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.readAsDataURL(blob);
  });
}

(6)blob → Object URL

const objectUrl = URL.createObjectURL(blob);

5. sendBeacon发送请求

XMLHttpRequest 是一种用于发送 HTTP 请求的 API,它需要设置请求头、处理响应等,比较麻烦,而且它会在主线程中创建一个新的 HTTP 请求,可能会阻塞主线程。当需要发送的数据量比较大时,使用 XMLHttpRequest 是可行的,但在埋点场景下,通常需要发送的数据量很小,而且需要以非阻塞的方式发送,这时 navigator.sendBeacon() 就更合适,因此我们有必要学习navigator.sendBeacon发送请求,它能够更有效地处理小数据量的后台传输。

navigator.sendBeacon() 用于将数据以非阻塞(后台)方式发送到服务器。此方法主要用于在网页会话期间定期发送小数据包,而不会影响页面的加载或用户交互。即使页面卸载(关闭)也会发送请求,解决了使用XMLHttpRequest发送同步请求而迫使用户代理延迟卸载文档的问题

语法:navigator.sendBeacon(url, data);

  • url:参数表明 data 将要被发送到的网络地址。
  • data参数是将要发送的 ArrayBufferArrayBufferViewBlobDOMStringFormDataURLSearchParams 类型的数据。

更详细内容可以看:

  1. MDN——Navigator.sendBeacon()
  2. navigator.sendBeacon

二、功能实现

文件目录结构:

-dist	        打包生成的文件夹,生成的子文件后缀都是根据rollup.config.js配置生成的
--index.cjs.js
--index.d.js
--index.ems.js
--index.js
-node_modules
--....
-src
--core           包的主文件,实现主要功能
---index.ts
--types          类型定义文件,定义了需要使用的类型
---index.ts
--utils          工具方法文件夹
---pv.ts         因为pv埋点要用的pushState()、replaceState()方法是没有原生事件的,所以这里写了一个触发这两方法时派发的事件
-index.html      测试埋点效果用的html文件
-package.json
-rollup.config.js打包配置文件
-tsconfig.json   ts配置文件

1. 安装依赖与配置打包命令

package.json

{
  "name": "tracker",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rollup -c"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "rollup": "^4.1.4",
    "rollup-plugin-dts": "^6.1.0",
    "rollup-plugin-typescript2": "^0.36.0",
    "typescript": "^5.2.2"
  },
  "dependencies": {
    "ts-node": "^10.9.1"
  }
}

2. rollup打包配置

rollup.config.js

import path from 'path';
import ts from 'rollup-plugin-typescript2';
import dts from 'rollup-plugin-dts';
const __dirname = path.resolve()
export default [
    {
        input: './src/core/index.ts',  // 入口文件
        output: [  // 输出文件
            {
                file: path.resolve(__dirname, './dist/index.ems.js'),
                format: 'es',  // 输出格式支持es规范,即import export
            },
            {
                file: path.resolve(__dirname, './dist/index.cjs.js'),
                format: 'cjs',  // 输出格式支持cjs规范,即require exports
            },
            {
                file: path.resolve(__dirname, './dist/index.js'),
                format: 'umd',  // 输出格式支持umd规范 即通用模块规范(amd和cjs的结合) amd(异步require) cmd() global都支持
                name: 'tracker'
            }
        ],
        plugins: [
            ts()  // 使用了这个 文件就支持ts 就会读取tsconfig.json
        ]
    },
    {  // 生成index.d.ts文件
        input: './src/core/index.ts',
        output: [  // 输出声明文件
            {
                file: path.resolve(__dirname, './dist/index.d.ts'),
                format: 'es'
            }
        ],
        // 该插件可以帮助我们自动生成 .d.ts 文件(TypeScript类型声明文件)
        plugins: [
            dts()
        ]
    }
]

// package.json中配置脚本 "build": "rollup -c" 运行脚本后就可以读取rollup.config.js文件
// tsconfig.json中配置 "module": "ESNext" ESNext泛指它永远指向下一个版本 如当前最新版本是ES2021 那么ESNext指的即是2022年6月要发布的标准

3. 类型定义

src/types/index.ts

/**
 * 这是一个默认值的接口,用于埋点类Tracker传递初识化时配置默认值
 * @uuid 做uv的 uv标识
 * @requestUrl 接口地址
 * @historyTracker history上报 单页面应用时 一种模式是hash一种模式是history
 * @hashTracker hash上报
 * @domTracker 携带Tracker-key 点击事件上报
 * @sdkVersionsdk sdk版本上报
 * @extra 透传字段 用户可以自定义一些参数 也可以上报这些
 * @jsError js 和 promise 报错异常上报
 */
export interface DefaultOptions {
    uuid: string | undefined,
    requestUrl: string | undefined,
    historyTracker: boolean,
    hashTracker: boolean,
    domTracker: boolean,
    sdkVersion: string | number,
    extra: Record<string, any> | undefined,
    jsError: boolean,
}

/**
 * @Options 继承于DefaultOptions
 * @Pirtial Partial实现将<>内的所有属性设置为可选。
 * 因此以下继承的默认参数都将是可传可不传的的
 */

export interface Options extends Partial<DefaultOptions> {
    requestUrl: string  // 这里又重写了requestUrl属性 意味着其他属性都是可传 但requestUrl必传
}

/**
 * version枚举
 */
export enum TrackerConfig {
    version = '1.0.0',
}

4. 实现pv操作事件监听

因为pv埋点要用的pushState()replaceState()方法是没有原生事件可以监听的,所以这里写了一个触发这两方法时派发同名事件的工具方法,通过监听派发的方法来实现pv埋点。

src/utils/pv.ts

/**
 * 因为pv埋点要用的pushState()、replaceState()方法是没有原生事件的,所以这里写了一个触发这两方法时派发的事件
 * PV: 页面访问量,即PageView,用户每次对网站的访问均被记录
 * 主要监听了history 和 hash
 * @type history内的方法名
 */
// 这里通过定义一个继承History对象泛型来限制传入的type
export const createHistoryEvent = <T extends keyof History>(type: T) => {
    
    const origin = history[type];  // 通过索引签名的方式拿到history内通过参数type传来的方法
    /**
     * 返回高阶函数
     * 这里的this是假参数 通过声明类型来欺骗编译器 以免下面使用this会有提示
     */
    return function (this: any) {
        // 使用apply()触发方法
        const res = origin.apply(this, arguments);
        /**
         * 使用Event创建自定义事件
         * @dispatchEvent 派发事件
         * @addEventListener 监听事件
         * @removeEventListener 删除事件
         * 其实就是 发布订阅模式
         */
        const e = new Event(type);
        // 派发参数传的type事件
        window.dispatchEvent(e);
        // 返回history[type]方法的返回结果
        return res;
    }
}
// 这里传入history.pushState
// createHistoryEvent('pushState')

5. 写一个简单接口测验埋点结果

这里新建了一个文件夹,使用Node.js写一个简易的接口http://localhost:9000/tracker模拟真实接口来校验埋点传参结果。

const express = require('express');
const cors = require('cors');

const app = express();
// 使用工具库cors解决跨域问题
app.use(cors());
app.use(express.urlencoded({ extended: false }));

app.post('/tracker', (req, res) => {
    console.log(req.body);  // 请求传来的参数
    res.send('ok')
})

app.listen(9000, () => {
    console.log('server is running on port 9000, success');
})

运行接口如下:

在这里插入图片描述

6. 核心代码

src/core/index.ts

import { DefaultOptions, TrackerConfig, Options } from "../types/index";
import { createHistoryEvent } from "../utils/pv";

// 鼠标事件列表
const mouseEventList: string[] = [
  "click",
  "dblclick",
  "contextmenu",
  "mousedown",
  "mouseup",
  "mouseenter",
  "mouseout",
];

export default class Tracker {
  // 暴露一个上报类
  public data: Options;
  constructor(options: Options) {
    // 传的默认参数
    // 传的参数覆盖默认兜底的参数
    this.data = Object.assign(this.initDef(), options);
    // 根据参数触发监听对应内容
    this.installTracker();
  }

  // 兜底逻辑:返回一些默认参数 里面也可以增加一些初始化的操作 因为构造函数内会执行这个兜底方法
  private initDef(): DefaultOptions {
    // history.pushState() 方法向浏览器的会话历史栈增加了一个条目。
    // replaceState()方法使用state objects, title,和 URL 作为参数,修改当前历史记录实体,如果你想更新当前的 state 对象或者当前历史实体的 URL 来响应用户的的动作的话这个方法将会非常有用。
    // history.replaceState(stateObj, "", "bar2.html");执行后会替换到bar2.html但不会加载bar2.html页面
    window.history["pushState"] = createHistoryEvent("pushState");
    window.history["replaceState"] = createHistoryEvent("replaceState");
    return <DefaultOptions>{
      sdkVersion: TrackerConfig.version,
      historyTracker: false,
      hashTracker: false,
      domTracker: false,
      jsError: false,
    };
  }

  /**
   * 捕获事件的监听器
   * @param mouseEventList 要监听的事件列表 是个字符串数组
   * @param targetKey 一个关键字 一般传给后台 要给后台协商
   * @param data 数据 可填可不填
   */
  private captureEvents<T>(eventList: string[], targetKey: string, data?: T) {
    eventList.forEach((event) => {
      // 监听
      window.addEventListener(event, (e: any) => {
        // 回调
        console.log("监听到了");
        // 调用接口实现上报
        this.reportTracker({
          event,
          targetKey,
          data,
        });
      });
    });
  }

  // 手动上报
  public sendTracker<T>(data: T) {
    // 调用接口实现上报
    this.reportTracker(data);
  }

  // 埋点触发器 根据传来的监听配置来判断监听哪些内容
  private installTracker() {
    // history上报
    if (this.data.historyTracker) {
      // 传入history上报需要监听的事件列表
      this.captureEvents(
        ["pushState", "replaceState", "popstate"],
        "pv-history"
      ); // 注意popstate是小写
    }
    // hash上报
    if (this.data.hashTracker) {
      this.captureEvents(["hashchange"], "pv-hash");
    }
    // dom上报
    if (this.data.domTracker) {
      this.domReport("dom");
    }
    //  js报错上报
    if (this.data.jsError) {
      this.jsError();
    }
  }

  // 接口实现上报
  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);
  }
  // uuid
  public setUserId<T extends DefaultOptions["uuid"]>(uuid: T) {
    this.data.uuid = uuid;
  }

  // 透传字段
  public setExtra<T extends DefaultOptions["extra"]>(extra: T) {
    this.data.extra = extra;
  }

  // dom事件上报
  private domReport(targetKey: string) {
    mouseEventList.forEach((ev) => {
      window.addEventListener(ev, (e) => {
        // console.log(e.target);
        const target = e.target as HTMLElement;
        if (target.getAttribute("target-key")) {
          console.log("监听到带有target-key属性元素的dom事件");
          this.reportTracker({
            event: ev,
            targetKey,
          });
        }
        console.log("未监听到带有target-key属性元素的dom事件");
        // let activeElement = document.activeElement;
        // if (activeElement?.getAttribute("target-key")) {
        //   console.log("监听到dom事件");
        // }
      });
    });
  }

  // 常规报错上报
  private errorEvent() {
    window.addEventListener("error", (event) => {
      console.log(event.message, "常规报错");
      this.reportTracker({
        event: "error",
        targetKey: "message",
        message: event.message,
      });
    });
  }
  // Promise报错上报
  private promiseReject() {
    window.addEventListener("unhandledrejection", (event) => {
      event.promise.catch((error) => {
        console.log(error, "promise报错");
        this.reportTracker({
          event: "unhandledrejection",
          targetKey: "message",
          reason: error,
        });
      });
    });
  }

  // js报错 包括常规报错和Promise报错
  private jsError() {
    this.errorEvent();
    this.promiseReject();
  }
}

7. 校验结果

我们在index.html实例化埋点类tracker并开启history、hash埋点、dom事件埋点、js报错埋点。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script src="./dist/index.js"></script>

    <!-- 给按钮添加自定义属性,如果有就dom上报 无则不上报 -->
    <button target-key="btn">按钮</button>
    <button>无添加</button>
    <script>
        new tracker({
            requestUrl: 'http://localhost:9000/tracker', // 请求的埋点接口
            historyTracker: true,
            hashTracker: true,
            domTracker: true,
            jsError: true
        })
        // console.log(sfafa);
    </script>
</body>

</html>

右键Open with Live Server运行html文件。

点击按钮按钮,可以看到触发如下四次接口,而点击无添加按钮并不会触发。

在这里插入图片描述

查看传参如下:

在这里插入图片描述

证明dom事件传参ok。

在控制台输入:history.pushState('state', 'title', 'a')跳转http://127.0.0.1:5501/a页并触发pv监听,结果如下

在这里插入图片描述

同样使用replaceState或者点击前进后退按钮一样会触发,则表面埋点是没有问题的。

在html页面的脚本内添加:console.log(sfafa);,由于sfafa未定义因此会触发js报错,通过这种方法我们来测试js报错埋点:

在这里插入图片描述

参考

小满埋点SDK从0开发并且发布npm (完结)

小满 前端埋点SDK 带你 从0 开发 并且发布npm

前端模块化详解(完整版)

安装 Rollup 以及 Rollup 和 Webpack 的区别

navigator.sendBeacon

谈谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1147584.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nginx 内存管理(二)

共享内存 共享内存结构与接口定义nginx共享内存在操作系统上的兼容性设计互斥锁锁的结构体锁的一系列操作&#xff08;core/ngx_shmtx.c&#xff09;创建锁 原子操作nginx的上锁操作尝试加锁获取锁释放锁强迫解锁唤醒等待进程 slab共享内存块管理nginx的slab大小规格内存池结构…

ctfshow-web入门命令执行29

29 源代码给了禁用flag 使用tac、nl ?cecho nl f*; ?cecho tac f*; 30 多禁用了system和php 和上题区别不大&#xff0c;使用上一题命令就能解 ?cecho nl f*; ?cecho tac f*; 31 禁用了空格使用%09代替 ?cecho%09tac%09f*; 32 禁用了echo 使用php伪协议 ?cinclud…

不做学习的奴隶,更要注重生活

下面是国外社交软件 i n s ins ins上近 40 40 40万点赞的帖子。 “睡8小时&#xff0c;而不是6小时。 锻炼1小时&#xff0c;而不是4小时。 学习3小时&#xff0c;而不是10小时。 读书2小时&#xff0c;而不是5小时。 深度工作3小时&#xff0c;而不是12小时。 你是人&#xff…

uniapp 模仿 Android的Menu菜单栏

下面这张图就是我们要模拟的菜单功能 一、模拟的逻辑 1. 我们使用uni-popup组件&#xff08;记得要用hbuilder X导入该组件&#xff09;uni-app官网 2. 将组件内的菜单自定义样式 二、uniapp代码 写法vue3 <template><view><uni-popup ref"showMenu"…

设计师在团队协作中的关键角色与策略

作为设计师&#xff0c;团队协作也是日常工作的一部分。在设计团队中&#xff0c;设计师如何参与团队协作&#xff1f;怎样才能更好的发挥自己的价值&#xff0c;顺利推进项目呢&#xff1f; 设计师遇到的协作难题&#xff1f; 首先我们看一下设计师在日常团队协作工作中可能…

C语言实现输入一个字符串,递归将其逆序输出

完整代码&#xff1a; // 输入一个字符串&#xff0c;递归将其逆序输出。如输入 LIGHT&#xff0c;则输出 THGIL #include<stdio.h> #include<stdlib.h> //字符串的最大长度 #define N 20//逆序输出字符串 void func(char *str){if (*str\0){//结尾时直接退出递归…

常见网络攻击及防御方法总结(XSS、SQL注入、CSRF攻击)

网络攻击无时无刻不存在&#xff0c;其中XSS攻击和SQL注入攻击是网站应用攻击的最主要的两种手段&#xff0c;全球大约70%的网站应用攻击都来自XSS攻击和SQL注入攻击。此外&#xff0c;常用的网站应用攻击还包括CSRF、Session劫持等。 1、 XSS攻击 XSS攻击即跨站点脚本攻击&am…

VBA宏查找替换目录下所有Word文档中指定字符串

原来搞质量管理&#xff0c;要替换质量文件里面所有特定名称或者某一错误时&#xff0c;需要逐一打开所有文件&#xff0c;非常麻烦&#xff0c;所以写了个VBA程序。过了这么多年&#xff0c;突然又要做同样的事情&#xff0c;发现新版本Word不支持其中的Application.FileSearc…

python自动化测试(五):按键模拟输入:全选、复制、清空、粘贴、完成

前置条件&#xff1a; 本地部署&#xff1a;ECShop的版本是3.0.0、Google版本是 Google Chrome65.0.3325.162 (正式版本) &#xff08;32 位&#xff09; Google驱动的selenium版本是3.11.0 目录 一、配置代码 二、键盘组合输入 2.1 全选&#xff1a;ctrl a 2.2 复制…

2023上半年系统集成项目管理工程师下午真题

文章目录 一&#xff1a;第5章 项目立项管理。第7章 项目范围管理&#xff0c;需求文件二&#xff1a;第9章 项目成本管理。第8章 项目进度管理&#xff0c;压缩工期三&#xff1a;第15章 信息&#xff08;文档&#xff09;和配置管理四&#xff1a;第18章 项目风险管理&#x…

ELASTICO-A Secure Sharding Protocol For Open Blockchains

INTRO 在中本聪共识中&#xff0c;通过POW机制来公平的选举leader&#xff0c;不仅非常消耗power&#xff0c;并且拓展性也不好。现在比特币中是7 TPS&#xff0c;和其他的支付系统相比效率相差甚远。 当前的许多拜占庭共识协议&#xff0c;并不支持在一个开放的环境中使用&a…

Linux 音频驱动实验

目录 音频接口简介为何需要音频编解码芯片&#xff1f;WM8960 简介I2S 总线接口I.MX6ULL SAI 简介 硬件原理图分析音频驱动使能修改设备树使能内核的WM8960 驱动alsa-lib 移植alsa-utils 移植 声卡设置与测试amixer 使用方法音乐播放测试MIC 录音测试LINE IN 录音测试 开机自动…

探索低代码PaaS平台的优势与选择原因

PaaS是一种云产品&#xff0c;它为应用程序的开发和部署提供基础结构。它提供中间件、开发工具和人工智能来创建功能强大的应用程序&#xff0c;大多数PaaS服务都与存储和网络基础架构捆绑在一起&#xff0c;就像基础架构即服务&#xff08;IaaS&#xff09;一样&#xff0c;可…

在Spring boot中 使用JWT和过滤器实现登录认证

在Spring boot中 使用JWT和过滤器实现登录认证 一、登录获得JWT 在navicat中运行如下sql,准备一张user表 -- ---------------------------- -- Table structure for t_user -- ---------------------------- DROP TABLE IF EXISTS t_user; CREATE TABLE t_user (id int(11) …

css文字竖向排列

div { writing-mode: vertical-rl;text-orientation: upright;font-size: .25rem; //文字大小letter-spacing: 0.1em; //文字间距}

Ubuntu安装ddns-go使用阿里ddns解析ipv6

Ubuntu安装ddns-go 1.何为ddns-go2.安装环境3.获取ddns-go安装包4.解压ddns-go5.安装ddns-go6.配置ddns-go 1.何为ddns-go DDNS-GO是简单好用的DDNS&#xff0c;它可以帮助你自动更新域名解析到公网IP。比如你希望在本地部署网站&#xff0c;但是因为公网IP是动态的&#xff0…

Transformer模型 | Python实现Attention-Transformer时间序列预测

时间序列预测 | Python实现Attention-Transformer时间序列预测 目录 时间序列预测 | Python实现Attention-Transformer时间序列预测基本介绍模型结构程序设计学习总结基本介绍 Python实现Attention-Transformer时间序列预测(TSAT model) 模型结构 main.py :含训练集合测试集的…

【图像分类】卷积神经网络之ResNet网络模型实现钢轨缺陷识别(附代码和数据集,PyTorch框架)

写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 本篇博文,我们将使用PyTorch深度学习框架搭建ResNet实现钢轨缺陷识别,附完整的项目代码和数据集,可以说是全网…

力扣:144. 二叉树的前序遍历(Python3)

题目&#xff1a; 给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 示例&#xff1a; 示例 1&#xff1a; 输…

spring框架回顾

如果是web项目,spring也推荐使用jar包而不是war包,因为如果是web项目,在下一页可以选择spring web,它里面就包含了spring mvc ,底层会有一个嵌入式的tomcat,到时候直接执行就可以了 正因为有此功能,所以把模块打成jar包后,可以直接java -jar [jar包名] 运行,直接可以在电脑访问…