埋点
概念以及应用场景
- pv(page view),同一用户对同一页面的访问次数
- uv ,独立用户访问次数
- 收集用户的隐私信息,优化性能体验,进行 A/B 业务决策
- 收集报错信息,进行监控
TIP
埋点肯定是跟用户有关的,需要获取系统里的用户信息
需要返回给后端的数据:
js
export default function user() {
return {
id: 1, // 用户id
name: '用户名', // 用户名
data: new Date().getTime(), // 当前时间
ua: navigator.userAgent, // 用户访问的设备信息
// 其他用户信息
...
}
}配置上报标记:
配置一个枚举用于标记需要上报的元素属性,当有这个自定义属性则需要上报
js
export enum Token {
click = 'data-click' // 标记需要上报的属性
}模拟一个需要上报的点击按键,在需要上报的元素打上属性,标记需要上报的交互元素
html
<body>
<button data-click="上报">上报</button>
<button>不上报</button>
<script type="module" src="./main.ts"></script>
</body>封装上报逻辑:
用于获取用户信息,上报埋点数据
js
import user from "./user";
import button from "./event/button";
class Tracker {
events: Record<string, Function>;
constructor() {
this.events = { button } // 注册方法
this.init();
}
/**
* 上报埋点
* @params 埋点信息
*/
protected sendReport(params = {}) {
let userInfo = user();
const body = Object.assign({}, userInfo, params);
// sendBeacon不支持跨域,不支持 JSON,需要通过Blob进行转换
let blob = new Blob([JSON.stringify(body)], {
type: "application/json",
});
navigator.sendBeacon("http://localhost:3000/tracker", blob);
}
// 遍历一下注册的方法,将上报埋点的逻辑sendReport传进注册的事件方法中
private init() {
Object.keys(this.events).forEach((key) => {
this.events[key](this.sendReport)
})
}
}WARNING
- axios,fetch,xhr 都会阻塞页面的关闭,当页面关闭时,接口停止
- 需要使用浏览器的 sendBeacon 方法,在页面关闭时继续发送请求,不会阻塞页面的关闭。
- 但是 sendBeacon 不支持跨域,不支持 JSON
定义 send 类型:
js
/**
* 开启功能 使用该sdk的人可以选择开启的功能
* button: true
*/
export interface Options {}
export type key<T = never> = "type" | "data" | "text" | T; // never在联合类型默认会被忽略
export type params = Record<key, any>; // 约束对象
export type send = (params: params) => void;
// 预期结构
// {
// type: "",
// data: {},
// text: ""
// }封装点击事件上报的函数:
js
import type { send } from '../type/index'
import { Token } from '../type/enum'
export default function button(send: send) {
// 获取上报的交换按键元素
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement
const flag = target.getAttribute(Token.click) // 获取标记的属性值
// 熟知按钮的位置
if (flag) {
send({
type: 'click',
text: flag,
data: target.getBoundingClientRect() // 获取按钮所在的位置信息
})
}
})
}监控代码报错,上报错误信息
可以再封装一个监控方法,记录报错信息,当代码遇到报错时,拦截并上报给后端
js
import { send } from "../type/index";
export default function error(send: send) {
window.addEventListener("error", (e) => {
send({
type: e.type,
data: {
line: e.lineno,
file: e.filename,
},
text: e.message,
});
});
}封装好报错监控之后,只需要直接在 Tracker 里注册就可以使用了:
js
import user from "./user";
import button from "./event/button";
import error from "./event/error";
class Tracker {
events: Record<string, Function>;
constructor() {
this.events = { button }
this.events = { button, error }
this.init();
}
/**
* 上报埋点
* @params 埋点信息
*/
protected sendReport(params = {}) {
let userInfo = user();
const body = Object.assign({}, userInfo, params);
// sendBeacon 不支持跨域,不支持 JSON
let blob = new Blob([JSON.stringify(body)], {
type: "application/json",
});
navigator.sendBeacon("http://localhost:3000/tracker");
}
private init() {
Object.keys(this.events).forEach((key) => {
this.events[key](this.sendReport)
})
}
}Promise 报错上报
如果需要拦截 promise,对 promise 报错进行上报,则可以添加以下方法:
js
import { send } from "../type/index";
export default function reject(send: send) {
window.addEventListener("unhandledrejection", (e) => {
send({
type: e.type,
data: {
reason: e.reason,
href: location.href
},
text: e.message
})
})
}TIP
当出现以下三种情况时,会触发 options 预检请求:
- 出现跨域情况
- 自定义请求头
- post 并且是 application/json 非普通请求
上报 ajax 或者 fetch 请求
如果需要上报 ajax,则需要重写 ajax,因为 ajax 没有中间件或者拦截器
ts
import { send } from "../type/index";
export default function request(send: send) {
const OriginOpen = XMLHttpRequest.prototype.open;
const OriginSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method: string, url:string) {
send({
type: 'ajax',
data: {
method,
url
},
text: 'ajax'
})
OriginOpen.call(this, method,url, async);
}
XMLHttpRequest.prototype.send = function (data) {
send({
type: 'ajax-send',
data,
text: 'ajax-send'
})
OriginSend.call(this, data);
}
// 处理fetch上报
const OriginFetch = window.fetch;
window.fetch = function (...args: any[]) {
send({
type: 'fetch',
data: args,
text: 'fetch',
})
return OriginFetch.apply(this, args)
}
}上报 PV
应用场景:当业务需求需要收集用户从 A 页面跳转到 B 页面的新旧 URL,上报到服务器,用于统计用户从页面 A 到 B 的访问量。
ts
import { send } from "../type/index";
export default function page(send: send) {
window.addEventListener('hashchange', (e) => {
send({
type: 'pv-hash',
data: {
newURL: e.newURL,
oldURL: e.oldURL
},
text: 'pv-hash'
})
})
// history 模式
window.addEventListener('popstate', (e) => {
send({
type: 'pv-history',
data: {
state: e.state,
url: location.href
},
text: 'pv-history'
})
})
// 手写pushState,解决history模式下,跳转其他页面不触发popstate的问题
const pushState = history.pushState;
window.history.pushState = function (state, titiel, url) {
const res = pushState.call(this, state, titiel, url);
// 注册自定义事件
const e = new Event('pushState');
window.dispatchEvent(e);
return res;
}
window.addEventListener('pushState', (e) => {
send({
type: 'pv-pushState',
data: {
url: location.href
},
text: 'pv-pushState',
})
})
}上报首屏加载时间
ts
import { send } from "../type/index";
export default function onePage(send: send) {
let firstScreenTime = 0;
// 监听dom的变化
const ob = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
firstScreenTime = performance.now()
})
if (firstScreenTime > 0) {
send({
type: 'firstScreen',
data: {
time: firstScreenTime
},
text: 'firstScreen'
})
ob.disconnect()
}
})
// subtree监听后代变化,childList监听增删改查
ob.observe(document.body, {subtree: true, childList: true})
}打包
在 vite.config.ts 中进行配置
ts
export default defineConfig({
build: {
lib: {
entry: './src/index.ts', // 入口文件
name: 'Tracker', // 全局变量的名称
fileName: 'tracker' // 打包后的文件名
format: ['es', 'cjs', 'iife', 'umd'] // 打包格式
}
}
})灰度发布
灰度发布,是指在发布新版本之前,先发布给部分用户进行测试,以确定新版本是否满足用户的需求或者是否存在 bug,新版本都没问题,再全量发布
- 如果灰度发布的新版本存在 bug,则通过埋点进行监控上报
TIP
软件版本号 1.0.0 -> 2.0.0 三段式版本号分别对应的含义:
- 第一位版本号更新代表整体架构发生了改变
- 第二位版本号更新代表功能发生了改变
- 第三位版本号更新代表 bug 的修复
小结
通过:通过埋点上报可以收集用户信息,错误反馈,性能数据,以便后续的更新与维护,通过单一职责的设计模式,对需要上报的内容进去封装并注册,即插即用
