前言
作为前端经常遇到线上报错却无法复现的情况,要是这个时候有错误监控能快速帮我们定位问题所在,再查找错误监控的相关资料尝试去实现一个自己的sdk。下面我以 错误监控
和设备信息
两方面作为关注点去实现。 完整代码
错误监控
运行时报错
window.onerror = (msg,url,lineNo,columnNo,e) => {
// ...上报错误
}
promise reject 未被处理
window.addEventListener('unhandledrejection',(event) => {
// 报错原因,当前路径,报错时间
const { message,config:{method,url} } = event.reason
// ...上报错误
})
重写原生的监听事件
// 保存原生的 addEventListener 事件
const originAddEventListener = EventTarget.prototype.addEventListener
// 重写原生的监听事件
EventTarget.prototype.addEventListener = (type,listener,options) => {
const wrappedListenner = (...args) => {
try {
return listener.apply(this,args)
} catch (error) {
const { name,message } = error
// ...上报错误
throw error
}
}
return originAddEventListener.call(this,type,wrappedListenner,options)
}
劫持 Vue.config.errorHandler
const vueErrorHandler = Vue.config.errorHandler
const wrapErrorHandler = function(err,vm,info) {
const componentRouteInfo = vm.$route
// 组件路径,路由名称
const { fullPath,name } = componentRouteInfo
// ...上报错误
vueErrorHandler.call(this,err,vm,info)
}
_Vue.config.errorHandler = wrapErrorHandler
设备信息的获取
页面滚动信息
// 滚动事件的监听
let scrollPosition = []
// 将滚动的一组数据进行上报,滚动时间超过设定时间进行上报 throttle 是自定义的节流函数
const scrollLog = throttle(() => {
// ...数据上报,并清空历史的位置点
scrollPosition = []
}, scrollTime)
function scrollHandler(e) {
// 如果监听的是 window 那么会在前三个值中拿到滚动距离
// 如果是设置了监听滚动的对象,那么会在 e.taget.scrollTop 中拿到滚动高度
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
// 将滚动的距离位置收集起来,可以进行间断上报,减少请求次数
// 记录的位置取整
scrollPosition.push(scrollTop.toFixed(0))
// ...数据上报
scrollLog(e)
}
const throttleScrollHandler = throttle(scrollHandler)
window.addEventListener('scroll',throttleScrollHandler)
点击事件
function clickHandler(e:any) {
const { target } = e
const clickInfo = judgeDomType(target)
logGif(clickInfo)
}
window.addEventListener('click',clickHandler)
// 获取目标元素的相关信息如: 当前元素的类名、元素标签、遍历第一个子元素的内容
function judgeDomType(target:HTMLElement) {
const { nodeType, nodeName, nodeValue,className,id} = target
let firstChild
switch (nodeType) {
// 元素类型
case Node.ELEMENT_NODE:
firstChild = searchBottomNestChild(target)
break
}
return {
firstChild,
selector: `class-${className};id-${id}`,
nodeName
}
}
// 获取该元素下嵌套的子元素的中第一个元素的文字
function searchBottomNestChild(dom:any) {
let current = dom
while (current.firstChild) {
current = current.firstChild
}
return current.nodeValue
}
设备相关信息
const pageLog = () => {
const userAgent = navigator.userAgent
let webview = ''
if (userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) {
webview = 'ios'
}
if (userAgent.match(/MicroMessenger\/([^\s]+)/)) {
webview = 'weixin'
}
if (userAgent.match(/QQ\/([\d\.]+)/)) {
webview = 'qq'
}
// 获取网络相关的信息
const connection = navigator.connection
return {
logType:LogType.page,
path:location.href,
platform:navigator.platform,
webview,
connection
}
}
发送数据
- 通过 new Image 方式创建的元素,只需要赋值 src 属性即可发送请求,无需插入文档中。
- 需要注意在拼接参数的时候,需要使用 encodeURIComponent 对值进行转移否则将 location.href 这类url作为值时会造成错误。
function parseJsonToString(dataJson) {
if (!dataJson ) { dataJson = {} }
var dataArr = Object.keys(dataJson).map(function(key) { return key + '=' + encodeURIComponent(dataJson[key]) })
return dataArr.join('&')
}
const logGif = (params) => {
const upload = parseJsonToString(params)
const img = new Image(1,1)
img.src = 'https://view-error?' + upload
}
打包sdk文件
完成了基本的错误监控功能后,我们可以把这些文件打包成一个js文件,其他项目需要应用的时候通过 script 引入的方式直接使用即可。
webpack.config.js 的配置会比我们项目中的少很多,由于我是使用的 typescript + vue 的方式,配置如下:
const path = require('path')
const resolve = dir => path.resolve(__dirname, dir)
module.exports = {
entry:{
// sdk文件的主入口
'view-error':resolve('../src/utils/index.ts')
},
output:{
path:resolve('../dist')
},
resolve: {
extensions:['.ts','.js']
},
module: {
rules:[
// 处理 .ts 结尾的文件
{
test: /\.tsx?$/,
loader: 'ts-loader'
}
]
}
}
服务端的处理
连接数据库
const mysql = require('mysql')
const pool = mysql.createPool({
host: 'ip',
user: 'admin',
password: 'password',
database: 'view_error'
})
// 接受 sql 语句,后续执行
function connect(sql) {
return new Promise((resolve, reject) => {
pool.getConnection((err, connection) => {
if (err) reject(err)
console.log('连接 成功')
connection.query(sql, function(error, results, fields) {
if (error) reject(error)
resolve(results)
connection.release()
})
})
})
}
插入数据
exports.insert = async function (ctx) {
const query = JSON.stringify(ctx.request.query)
// 这里直接将客户端传来的值插入,前期实验用,后续需要优化的地方
await connect(`insert into view_error (root) values('${query}')`)
// 图片作为返回
const file = fs.readFileSync('upload/icon-image.gif')
ctx.type = 'image/gif'
ctx.body = file
}
疑问和待优化
问题
- 目前是通过 vue-router 来监听页面跳转的。我试了下
window.addEventListener('popstate',()=>{})
和window.addEventListener('hashchange',()=>{})
都监听不到 vue 页面路由的变化。js原生能否监听路由变化? - 想记录单个用户的操作链路,如何将用户链路关联起来,通过同ip关联吗,
- 服务端存储的时候该怎么归类存储查询比较方便快捷,服务端这块不是很熟悉。
待优化
- 前端发送数据的时候统一格式,将请求分为几类或者相应的等级。
- 优化客户端采集的信息,以及一些报错信息的捕获。
- 服务端相同信息归类存储,优化存储空间。