前言
为什么要做前端监控?
我们在写程序时也许会考虑以下几个问题:
-
我们写的功能是否有人在用?
-
新的功能是否稳定?有没有逃逸的线上缺陷?
-
产品的数据是否可以积累?产品是不是还有优化的空间?
在当今的互联网世界中,网站和应用程序的 性能
、稳定性
和 用户体验
至关重要。我们需要一个脚本能实时监控我们的程序,对用户使用情况进行跟踪,对程序运行错误进行提前预警和定位,对产品使用数据进行累计从而为程序产生 可持续
的发展方向。

今天就由小凌带大家搭建一个前端埋点监控小 demo
,让大家对前端埋点监控有个初步的认知。
前端监控目标
我们需要一个这样的程序,它可以收集用户日常产生的错误从而提高产品的稳定性。
能监控用户的体验

最好在使用上能更简便,无需复杂的学习和繁琐的引入方式。
对老业务无影响,后期好维护,能可持续发展。
范围与实现流程
前端目前的需要监控的埋点位置有 页面加载
、按钮点击
、表单提交
、JavaScript 报错
、页面加载超时
、用户请求完成
等。
今天我们主要对 JavaScript 报错进行埋点。监测若 JavaScript 代码运行出错则向日志服务器发送一条埋点请求,带上相关报错信息。
该流程主要分为如下几个阶段:
-
用户触发页面报错
-
监听到出错并调用相关方法
-
收集用户信息以及报错位置信息
-
请求发送埋点接口,将数据发送至云平台或后台
-
查看相关埋点信息在云平台或后台相关的记录是否正确

实践
构建项目
使用webpack初始化一个项目
csharp
npm init -y
下载我们本次需要的依赖包
css
npm install html-webpack-plugin user-agent webpack webpack-cli webpack-dev-server
完整 webpack.config.js 配置
js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
mode: 'development',
context: process.cwd(),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
devServer:{
static: path.resolve(__dirname, 'dist'),
},
plugins:[
new HtmlWebpackPlugin({
template:'./src/index.html',
inject:'head',
})
]
}
目录布局与数据构思
我们需要提前规划好该项目的整体文件布局

也需要提前构思好需要记录的数据有哪些?这些属性名
分别是什么?

主页入口
我们需要编写一个可以触发 JavaScript 代码报错的场景,这边我们使用 a.b.c 找不到属性的错误出发场景。
src/index.html
html
<!--html-->
<button class="error-button" onclick="errorClick()">点此抛出错误</button>
<!--js-->
<script>
function errorClick(){
// 点击时候抛出错误
window.a.b = 'no find error'
}
</script>
对于需要引入的主要依赖 JavaScript 文件,我们直接让其引入 lib 文件夹中的方法并执行。
src/index.js
js
import { injectJsError } from "./monitor/lib/jsError.js";
injectJsError();
错误捕获
在 lib 中我们申明一个专门捕获 JavaScript 报错的文件。
src\monitor\lib\jsError.js
js
import { getLastEvent, getSelector, getLines } from '../utils';
import tracker from '../utils/tracker';
// 对js报错进行监听
export const injectJsError = () => {
window.addEventListener('error', (e) => {
const lastEvent = getLastEvent(); // 获取最后一个事件
let log = {
kind: 'stability',
type: 'error', // 埋点类型
errorType: 'JSError', // 具体错误
url: window.location.href, // 访问路径
message: e.message, // 错误信息
fileName: e.fileName, // 错误文件名
position: `${e.lineno}:${e.colno}`, // 错误位置
stack: getLines(e.error.stack), // 错误堆栈
selector: lastEvent ? getSelector(lastEvent.composedPath()) : '' // 最后一个操作的元素
}
// 发送埋点数据
tracker.send(log)
})
}
可以看到,我们在这个 js 中监听了 window 的 error 事件,并在其参数中获取到了我们想要的信息。
这边前面我们拟定的传输数据属性固定就起到了作用,我们很明确我们需要什么内容。
为保证该 js 的低耦合,我们把相关数据转换和接口请求放到了外部引入。
数据转换与获取
堆栈内容可读性转换

src\monitor\utils\index.js
js
export const getLines = (stack) => {
return stack.split('\n').reverse().map(item => {
return item.replace(/^\s+at\s+/g, '')
}).join('\n')
}
最后触发元素记录
这里我们在 js 载入时先对所有的用户操作进行记录,如果触发了相关事件则替换最后触发元素的变量。
src\monitor\utils\index.js
js
const EVENTS = ['click','touchstart','mousedown','keyDown','mouseover'];
let lastEvent;
EVENTS.forEach((eventType) => {
document.addEventListener(eventType, (event) => {
lastEvent = event
},
{
capture: true, // 捕获阶段
passive: true // 冒泡阶段
})
})
export const getLastEvent = () => {
return lastEvent
}
通过 getLastEvent
方法我们便可获得用户最后操作的元素。
获取最终触发元素的路径
这个仍然是一个转换方法,我们需要先取出无用的最外层 document
和 window
src\monitor\utils\index.js
js
export const getSelector = (path) => {
let selectorPathStr = ''
if(path.length){
selectorPathStr = path.reverse()
.filter(element => element !== document && element !== window )
.map(element => {
let selector = ''
if(element.id){
return `${element.nodeName.toLowerCase()}#${element.id}`
}else if(element.className && typeof element.className === 'string'){
return `${element.nodeName.toLowerCase()}.${element.className}`
}else{
selector = element.nodeName.toLowerCase()
}
return selector
}).join(' ')
}
return selectorPathStr
}
这样我们便可以得到最终触发问题的节点位置,如下:

阿里云日志SLS对接
我们需要一个 接口
来获取我们的埋点信息并进行持久化。此处我们使用目前比较成熟 阿里云日志SLS ,将我们的日志信息上传到云。当然,你也可以搭建后台地址,提供一个可持久化数据的接口来保存埋点信息。
Project 创建
创建 Project

此处选择打开

选择接入方式 WebTracking

下一步

打开控制台发现无数据

接口对接
找到 PutWebtracking 帮助文档,查看对接具体参数。
在真正接通之前,我们可以用 Postman 来对接口进行测试。

js
http://${PROJECT}.log.aliyuncs.com/logstores/${STORE}/track
其中 PROJECT
和 STORE
都可以在控制台上获得。
额外参数
当然我们还要获取用户的操作时间和浏览器与系统信息,方便我们定位错误位置。
src\monitor\utils\tracker.js
js
// 获取额外参数
const getExtraData = () => {
return {
title: document.title, // 网页标题
timestamp: `${new Date().getTime()}`, // 时间戳(阿里云只接收String格式字符串)
userAgent: userAgent.parse(navigator.userAgent).full // 用户系统与浏览器信息
}
}
// 发送给接口的日志信息
const log = {
...getExtraData(),
...data // 前面封装的信息
}
完整的请求方法
js
class SendTracker{
constructor(){
this.url = `${HOST}/logstores/${STORE}/track`
this.xhr = new XMLHttpRequest()
}
send(data = {}){
this.xhr.open('POST', this.url, true)
const log = {
...getExtraData(),
...data
}
for (let key in log){
// 阿里云限制必须要String
if(typeof log[key] === 'unmber'){
log[key] = `${log[key]}`
}
}
const body = JSON.stringify({
"__topic__": STORE,
"__source__": "my-source",
"__logs__": [
{...log}
],
})
this.xhr.setRequestHeader('Content-Type', 'application/json')
this.xhr.setRequestHeader('x-log-apiversion', '0.6.0')
this.xhr.setRequestHeader('x-log-bodyrawsize', body.length)
this.xhr.send(body)
this.xhr.onload = () => {
console.log(this.xhr.response)
}
this.xhr.onerror = (error) => {
console.log(error)
}
}
}
export default new SendTracker;
测试
一切都设置成功后,当我们点击"点此抛出错误"按钮的时候,就会向阿里云发送一次请求

在后台我们便可看到响应的数据已经写入

打包后点击 index.html
中的按钮正常请求

如此,外部使用只要引入打包好的 bundle.js
便可引入我们的 埋点功能
。
优化
当然你也可以优化它:
1.使用 TypeScript
规范化它的变量类型和全局枚举设置。

- 使用
node.js
将数据存储为本地信息。只需要我们提供一个接口将数据接入本地即可。值得一提的是数据格式尽量保持一致,方便后续的数据迁移
。
