前端实现埋点监控

文章目录

    • 一、埋点&监控
    • 二、前置知识
      • 1. 区分JS模块化
      • 2. rollup
      • 3. History
        • 3.1 history.pushState()
        • 3.2 history.replaceState()
        • 3.3 popstate事件
      • 4. JS二进制
        • 4.1 Blob
        • 4.2 File
        • 4.3 FileReader
        • 4.4 ArrayBuffer
        • 4.5 Object URL
        • 4.6 Base64
        • 4.7 格式转换
      • 5. sendBeacon发送请求
    • 三、功能实现
      • 1. 安装依赖与配置打包命令
      • 2. rollup打包配置
      • 3. 类型定义
      • 4. 实现pv操作事件监听
      • 5. 写一个简单接口测验埋点结果
      • 6. 核心代码
      • 7. 校验结果
      • 参考

一、埋点&监控

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

  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的urlreader.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);}// uuidpublic 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.hqwc.cn/news/312473.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【AI】人类视觉感知特性与深度学习模型(2/2)

目录 二、人类视觉感知特性对深度学习模型的启发 2.1 视觉关注和掩盖与调节注意力模型的关系 1.视觉关注和掩盖 2. 注意力机制模型 2.2 对比敏感度与U形网络的联系 2.3 非局部约束与点积注意力的联系 续上节 【AI】人类视觉感知特性与深度学习模型&#xff08;1/2&#…

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK设置相机本身的数据保存(CustomData)功能(C#)

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK设置相机本身的数据保存&#xff08;CustomData&#xff09;功能&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机的数据保存&#xff08;CustomData&#xff09;功能的技术背景CameraExplorer如何使用图像剪切&#xff…

【后端】Docker学习笔记

文章目录 Docker一、Docker安装&#xff08;Linux&#xff09;二、Docker概念三、Docker常用命令四、数据卷五、自定义镜像六、网络七、DockerCompose Docker Docker是一个开源平台&#xff0c;主要基于Go语言构建&#xff0c;它使开发者能够将应用程序及其依赖项打包到一个轻…

Visual Studio 2015 中 SDL2 开发环境的搭建

Visual Studio 2015 中 SDL2 开发环境的搭建 Visual Studio 2015 中 SDL2 开发环境的搭建新建控制台工程拷贝并配置 SDL2 开发文件拷贝 SDL2 开发文件配置 SDL2 开发文件 测试SDL2 开发文件的下载链接 Visual Studio 2015 中 SDL2 开发环境的搭建 新建控制台工程 新建 Win32 …

vue-springboot基于JavaWeb的汽配汽车配件销售采购管理系统

过对知识内容的学习研究&#xff0c;进而设计并实现一个基于JavaWeb的汽配销售管理系统。系统能实现的主要功能应包括&#xff1b;汽车配件、销售订单、采购订单、采购入库等的一些操作&#xff0c;ide工具&#xff1a;IDEA 或者eclipse 编程语言: java 数据库: mysql5.7 框架&…

OFDM——PAPR减小

文章目录 前言一、PAPR 减小二、MATLAB 仿真1、OFDM 信号的 CCDF①、MATLAB 源码②、仿真结果 2、单载波基带/通频带信号的 PAPR①、MATLAB 源码②、仿真结果 3、时域 OFDM 信号和幅度分布①、MATLAB 源码②、仿真结果 4、Chu 序列和 IEEE802.16e 前导的 PAPR①、MATLAB 源码②…

C#编程-编写和执行C#程序

编写和执行C#程序 可以使用Windows记事本应用程序来编写C#程序。在记事本应用程序中创建C#程序后,您需要编译并执行该程序以获得所需的输出。编译器将程序的源代码转换为机器代码,这样计算机就能理解程序中的指令了。 注释 除了记事本,您还可以使用任何其他文本编辑器来编写…

MAC 中多显示器的设置(Parallels Desktop)

目录 一、硬件列表&#xff1a; 二、线路连接&#xff1a; 三、软件设置&#xff1a; 1. 设置显示器排列位置及显示参数 2. 分别设置外接显示器为&#xff1a;扩展显示器&#xff0c;内建显示器为主显示器 3. 设置Parallels Desktop屏幕参数 四、结果 一、硬件列表&a…

C++实现定积分运算

文章目录 题目代码 题目 代码 #include <iostream> #include <cmath> #include <functional>using namespace std;// 定积分函数 double integrate(function<double(double)> func, double a, double b, int num_intervals) {double h (b - a) / num…

使用STM32实现多设备UART通信指南

本文将介绍如何在STM32上实现多设备UART通信&#xff0c;包括配置多个UART接口、数据的发送和接收&#xff0c;以及如何有效地进行多设备通信。我们将使用STM32CubeMX和HAL库来演示配置过程&#xff0c;并给出相关的示例代码和技巧。UART&#xff08;Universal Asynchronous Re…

怎么解决 Nginx反向代理加载速度慢?

Nginx反向代理加载速度慢可能由多种原因引起&#xff0c;以下是一些可能的解决方法&#xff1a; 1&#xff0c;网络延迟&#xff1a; 检查目标服务器的网络状况&#xff0c;确保其网络连接正常。如果目标服务器位于不同的地理位置&#xff0c;可能会有较大的网络延迟。考虑使用…

灸哥问答:测试架构师应该掌握哪些技能?

测试架构师是软件测试领域的高级职位&#xff0c;在承担工作时需要掌握多方面的技能和能力以确保测试过程的有效性、高效性和可靠性。从我的经历和认知角度&#xff0c;我觉得作为测试架构师应该掌握具备以下技能&#xff1a; 一、测试方法和策略 掌握不同的测试方法&#xf…