Appearance
埋点监控
为什么要做埋点监控? 埋点sdk
专业名词解释
这里对一些专业名词进行解释
一般都是c端用的比较多 就是为了收集用户的浏览隐私 优化A/B业务
pv (page view) 同一用户对同一页面访问的次数
uv 是不同用户对页面的访问次数 是通过用户的ip地址来进行区分
灰度 软件app用的比较多 就是我们的软件用100万用户 现在有个1.0.0 => 2.0.0的转换 我们先从 5%的用户发布 如果没问题再 30% 最后再全量发布
埋点&监控能做什么
从单个页面的常规数据角度出发我们可以通过埋点获取:访问次数(UV/PV)、地域数据(IP)、在线时长、区域点击次数等数据。
当我们将这些单点数据按照特定的纬度进行数据聚合,就可以获得全流程视角下的数据如:用户留存率/流转率、用户转化率、用户访问深度等数据。
而在埋点数据进行上报的同时,我们也可以同步收集页面基础数据/接口相关数据如:页面加载/渲染时长、页面异常、请求接口等数据。
同时对于前端监控来说,大致可以分成三个方向:数据监控、性能监控、异常监控。
具体操作案例代码:
1.先确认我们需要安装的依赖包
package.json
json
{
"name": "my-point-monitor",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/nodemailer": "^6.4.14",
"vite": "^5.1.6"
},
"dependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"express": "^4.18.3", // 通过express搭建一个小型服务器 接收发送请求
"ioredis": "^5.3.2", //一个100%用TypeScript编写的redis插件,做缓存使用
"nodemailer": "^6.9.12", // 通过这个插件 来进行监控信息发送到邮箱
"vue": "^3.4.21"
}
}
2.演示demo项目结构

index.html文件
html
index.html
<!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>
<div id="app"></div>
<button data-click="上报">上报</button> // 首先 我们假设页面中有点击事件进行监控上报
<button>不上报</button>
<script type="module" src="./main.ts"></script> // 模块化导入方式
</body>
</html>
3.创建index.ts文件
- 这一部分是入口 我们编写的代码文件的总入口
- 编写Tracker类 写上报埋点的方法
ts
import user from "./user";
import button from "./event/button";
import error from "./monitor/error";
import reject from "./monitor/reject";
import request from "./monitor/request";
import pv from "./pv";
import page from "./page";
class Tracker {
events: Record<string, any>;
constructor() {
this.events = { button, error, reject, request, pv, page };
this.init();
}
/**
* 上报埋点
* @param params 这就是我们需要传递的埋点信息
*/
//这里就是上报埋点
protected sendRequest(params = {}) {
let userInfo = user();
const body = Object.assign({}, userInfo, params);
//当我们想到要上报埋点的时候 我们就需要想到navigator.sendBeacon 为什么?
// 因为如果我们使用xhr, ajax, axios,fetch等方法的话 都是不行的 为什么
// 因为这些方法都是异步的, 当页面关闭的时候, 这些异步请求就会被终止, 所以我们需要使用navigator.sendBeacon
//navigator.sendBeacon 他会在页面关闭的时候, 依然会发送请求
/**
* sendBeacon的缺点是 只能用post发送我们的信息 并且sendBeacon不能接受json类型的参数 并且不支持跨越
* 但是sendBeacon可以发送blob类型的参数
* 而我们的blob类型的参数 可以用一个 JSON 字符串构造一个 blob 那里面装的就是json字符串
*/
const blob = new Blob([JSON.stringify(body)], {
type: "Application/json", //其实我们正常的数据类型是URLSearchParams querystring 像Application/json这种是非正式的格式 这是我们自己定义的
});
navigator.sendBeacon("http://localhost:3000/tracker", blob);
}
private init() {
Object.keys(this.events).forEach((key) => {
this.events[key](this.sendRequest);
});
}
}
export default Tracker;
4.创建user/index.ts 模拟公司返回用户的身份信息以及id
ts
export default function user() {
return {
id: 1,
name: "xiaoliao",
age: 18,
ua: navigator.userAgent,
};
}
5.创建pv/index.ts
ts
import type { send } from "../type";
export default function pv(send: send) {
/**
* 这里我们监听的就是 当我们的页面跳转的时候 那么这里我们就会用到跳转的方法
* 原生的js里面 涉及到页面跳转的方法有两个
* 1.hashChange
* 2.pushState 这两个方法都是让页面可以进行跳转的方法
*/
//hashChange可以通过hashChange事件来进行监听
window.addEventListener("hashchange", (e) => {
send({
type: e.type,
data: {
newURL: e.newURL,
oldURL: e.oldURL,
},
text: e.type
})
});
//还有一种方式 就是通过history进行跳转
//但是目前只有前进和后退可以监听到 我们不能直接去监听到pushState的方法
window.addEventListener('popstate', (e) => {
send({
type: e.type,
data: {
state: e.state,
url: location.href
},
text: e.type
})
})
/**
* 那我们就该重写pushState方法 去让pushState可以监听到
*/
const OriginPushState = history.pushState
history.pushState = function (params, unsed, url) {
const res = OriginPushState.call(this, params, unsed, url)
send({
type: 'history-push',
data: {
params, unsed, url
},
text: 'history-push'
})
return res
}
}
6.创建page/index.ts
- 计算首屏加载时间
ts
import { send } from './../type/index';
/**
* 这个方法就是去计算首屏加载时间 并且发送到后端 而且我们可以通过获取的首屏加载时间做一个统计图 去给领导呈现项目的效率
*/
export default function page(send: send) {
//我们如何去监听 项目的首屏什么时候加载完呢? 就是当我们的项目启动的时候 vue会往我们的dom元素去插入东西
//意思就是vue把所有的js执行完后 就会开始渲染dom到我们的<div id=""></div>盒子里面
//所以我们只需要去监听 我们的html盒子是否已经有元素变化就行了
/**
* 监听元素的变化就是用mutationObserver
*/
let firstTime = 0 //首屏加载时间
const observer = new MutationObserver((mutations) => {
mutations.forEach(item => {
firstTime = performance.now()
if (firstTime > 0) {
send({
type: 'page',
data: {
firstTime
},
text: 'page'
})
observer.disconnect()
}
})
})
observer.observe(document, {subtree: true, childList: true}) // 但是我们这里监听不到这个document.body 因为我们目前还没有创建vue文件
// 所以我们可以监听document 因为vue会强制性的在document去插入一个结构
}
7.创建monitor文件夹
分别创建error.ts reject.ts request.ts文件
error.ts
ts
import type { send } from "../type";
export default function error(send: send) {
window.addEventListener("error", (e) => {
send({
type: e.type,
data: {
lineno: e.lineno,
filename: e.filename,
},
text: e.message,
});
});
/**
* 当我们已经接受到了错误的信息的时候 我们就需要去给部门或者是前端发送一个消息
*/
}
reject.ts
ts
import type { send } from "./../type/index";
export default function reject(send: send) {
/**
* Promise.reject('error') 是通过unhandledrejection
*/
window.addEventListener('unhandledrejection', (e) => {
console.dir(e)
send({
type: e.type,
data: {
reason: e.reason,
href: window.location.href //我们尽可能的去添加一些报错信息 这里是我们自己添加的当前页面的报错信息
},
text: e.reason
})
})
}
request.ts
ts
import type { send } from "./../type/index";
/**
* 我们这里的目的是为了去监控我们发起请求的时候 但是我们这里可以自己去做一些操作 去判断什么请求需要去发送一个埋点信息
* 这个页面的作用是 让我们知道每个接口都发了什么做了什么
*/
export default function request(send: send) {
//但是我们没有可以获取ajax请求的监听事件 所以我们需要重写方法
const OriginOpen = XMLHttpRequest.prototype.open;
const OriginSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (
method: string,
url: string | URL,
async: boolean = true
) {
send({
type: "ajax",
data: {
method,
url,
},
text: "ajax",
});
OriginOpen.call(this, method, url, async);
};
XMLHttpRequest.prototype.send = function (data: any) {
send({
type: "ajax-send",
data,
text: "ajax-send",
});
OriginSend.call(this, data);
};
const OriginFetch = window.fetch;
window.fetch = function(...args: any[]) {
send({
type: 'fetch',
data: args,
text: 'fetch'
})
return OriginFetch.apply(this, args);
}
}
8.创建event文件夹下再创建button.ts
ts
import type { send } from "../type";
import { Token } from "../type/enum";
export default function button(send: send) {
window.addEventListener('click', (e) => {
const target = e.target as HTMLElement
console.dir(target)
//我们需要去把当前按钮的位置信息也给传递过去 这样我们才能知道是哪一个页面或者是哪一个位置点击的最多
//通过什么过去一个元素的位置呢?
//通过getBoundingClientRect去获取
//可以通过target去获取
if (target.getAttribute(Token.click)) {
send({
type: 'click',
data: {
rect: target.getBoundingClientRect(),
href: location.href
},
text: Token.click
})
}
})
}
9.创建server文件夹index.ts
根据server文件夹的index.ts 创建一个服务器
并且根据nodemailer创建邮件传输
ts
import express from "express";
// import Redis from "ioredis";
import mailer from "nodemailer";
const transporter = mailer.createTransport({
service: "qq",
host: "smtp.qq.com",
port: 465,
auth: {
user: "708880503@qq.com",
pass: "iqarvetzmtpfbbed"
}
})
// 创建redis实例
const redis = new Redis({
host: "127.0.0.1",
port: 6379,
});
const app = express();
//但是我们这里就是express其实是不允许我们发起post请求的 所以这个时候我们需要使用一个中间件
//除了在前端可以设置跨域以外 我们可以在服务器中设置以允许跨域请求
/**
* 这里就是我们用来use * 代表所有的请求都会往这里面走
* *是不允许上传cookies (准确的来说 是因为上传了cookies 浏览器的同源政策会管理的更严格 就要指定 不能允许所有跨域地址)
*/
/**
* options预检请求(只有三种方式才会出现)
* 1.跨域
* 2.自定义请求头
* 3.post请求并且传的是application/json的方式 非普通请求
*/
app.use("*", (req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); //不能用* 因为当前的navigator.sendBeacon是会携带cookies进行发起请求
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Headers", "Content-type, Authorization"); //因为application/json是我们自己定义的 我们就要给请求头 设置content-type 并且为
/**
* 但是我们的cors只允许普通请求 例如URLSearchParams text/plain query formData 这几类 而application/json不是普通请求 是我们人为定义的类型
* 并且跨域资源共享(cors)是不允许携带凭证的 凭证是什么? 凭证就是 cookies 以及 https证书等等
* 所以我们要允许让请求携带cookies 前端的cookies就是后端的session 后端会种cookies到浏览器 不同的cookies代表唯一标识 代表是哪个用户
*/
next(); //这里必须要用next 不然不往下走
});
app.use(express.json());
app.listen(3000, () => {
console.log("server start");
});
app.post("/tracker", (req, res) => {
console.log(req.body);
//这个lpush 是队列的意思
redis.lpush("tracker", JSON.stringify(req.body)); //这个当我们重启服务 内存里面的东西就会丢失 那要
if (req.body.type === 'error' || req.body.type === 'unhandledrejection') {
transporter.sendMail({
from: '708880503@qq.com',
to: '708880503@qq.com',
subject: '错误监控',
text: JSON.stringify(req.body)
})
}
return res.send("ok"); //这里发送的消息越少越好 为什么? 因为我们一个项目有很多地方都需要埋点 如果每一个都返回大量的信息 那我们的项目得有多少信息返回啊 所以说返回的信息越少越好
});