# 搭建前端监控系统
# 前言
有两种做法
- 使用开源项目,例如sentry,fundebug等。优点是方便,接入即可,缺点是需要银子。
- 自己搭建。通过监听、劫持各种事件,处理信息后上报。
sentry可以自己搭建环境就不用收费,那么先来讲讲sentry在vue中的使用。
# sentry
绝大部分内容来自掘金作者:夜_游神
https://juejin.im/post/5eeaebf2f265da02a87f00bf
# 注册账户
- 首先注册一个账户(测试玩玩可以用自己的,公司项目还是用公司邮箱吧),注册时组织千万别乱填,这玩意很重要,后面很多地方都需要
- 注册账户完成后注册项目
项目名称也不能乱写后面也有用的,选择自己的语言(vue居然还要搜索,我大VUE为何沦落至此?)
注册完项目后你就做好了前置准备了
# 引入项目
cnpm install @sentry/browser
cnpm install @sentry/integrations
//在main.js中引入
import Vue from 'vue'import * as Sentry from '@sentry/browser';
import { Vue as VueIntegration } from '@sentry/integrations';
Sentry.init({
dsn: 'https://32ad19d9b80448f593f1df2833256d23@o406425.ingest.sentry.io/5273959',
integrations: [new VueIntegration({Vue, attachProps: true})],
})
这时简单制造个报错,即可在控制台看到sentry的接口上报
高兴之余仔细一看,这特瞄的都报的啥错?完全看不懂啊?因为正式环境的代码都是进行了丑化,没有.map文件就是报错了你也不知道具体位置。这知道和没知道简直没有区别啊
这时候我们就要进入第二步,将.map文件上传到服务器
# 上传sourceMap
一:
因为公司项目还是vue-cli2,这里就是自己研究的
打开package.json,看build命令,调的是build/build.js
进到build.js中看到配置是从config/index,js及webpack.prod.conf中取得
进config/index,js,发现productionSourceMap,置true即可
来到webpack.prod.conf中,因之前换成了ParallelUglifyPlugin插件进行打包,在其配置中开启sourcemap
new ParallelUglifyPlugin({
cacheDir: '.cache/', // 设置缓存路径,不改动的调用缓存,第二次及后面build时提速
uglifyJS:{
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
// drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
sourceMap: true
}),
往下看,还有webpackConfig得改
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: true,
extract: true
})
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath(`js/[name].[chunkhash].js`),
chunkFilename: utils.assetsPath(`js/[id].[chunkhash].js`)
},
plugins: plugins
})
此处devtool不开启貌似就打包不了map,开启了浏览器又报找不到map文件的warning(map没上传的服务器),应该可以通过限制ip访问解决map泄露问题。
好,npm run build看看是否有map文件生成了。
二:
将打包的文件自动上传的sentry服务器,这时候我们需要一个webpack插件@sentry/webpack-plugin 首先就是安装插件
cnpm i @sentry/webpack-plugin --save-dev
然后在根节点处创建文件.sentryclirc(标准配置文件的命名方式)
[auth]
token=token // 无需引号
[defaults]
url = https://sentry.io
org = 前面提到的组织名
project = 前面提到的项目名
token获取方式
如果没有token就注册一个
在config文件夹中新建一个sentry.env.js
,带上RELEASE_VERSION,用来标记上传的版本号
const release = "test@1.0.0";
process.env.RELEASE_VERSION = release;
module.exports = {
NODE_ENV: '"sentry"',
RELEASE_VERSION: `"${release}"`
}
注意写法,试了'"test@1.0.0"'
不行,不知道为啥
换成上面的即可
然后在webpack.prod.conf中引入插件
const SentryCliPlugin = require('@sentry/webpack-plugin')
configureWebpack:{
plugins:[
new SentryCliPlugin({
include: "./dist/", // 作用的文件夹,如果只想js报错就./dist/js
release: process.env.RELEASE_VERSION, // 一致的版本号
configFile: "sentry.properties", // 不用改
ignore: ['node_modules', 'webpack.config.js'],
urlPrefix: "~/",//这里指的你项目需要观测的文件如果你的项目有publicPath这里加上就对了
})
]
}
在插件初始化的地方也要加上版本号 所以main.js也要改成
Sentry.init({
dsn: 'XXX',
integrations: [new VueIntegration({Vue, attachProps: true})],
release: process.env.RELEASE_VERSION
});
这时打包,需要上传,时间比较久
成功后去sentry上看看
这里我们看到这就是我们之前定义的项目名称,点击项目名称
这时候我们部署上去(这里测试可以在本地跑个服务,做个简单的报错比如abdc(),就会报abcd不是方法),报个错看看
可以看到报错的源代码了,真香~
上传了sourseMap后还存在一些问题,比如开发调试的时候,报错也会被收集,那么多错,我看的岂不是头都大了,同时开发时报错不会再显示,就会为开发造成困扰,这次我们着重解决这些问题。
# 正式环境与测试环境区分
先在package.json加条新命令
"build--sentry": "cross-env NODE_ENV=sentry node -max_old_space_size=4096 build/build.js",
在main.js中sentry初始化时区分环境,如果不是正式环境就不在初始化就好;
process.env.NODE_ENV === 'sentry' && Sentry.init({
dsn: 'https://787297c8c01b45d2a6325edcc3c88275@o413596.ingest.sentry.io/5300418',
integrations: [new VueIntegration({Vue, attachProps: true})],
release: process.env.RELEASE_VERSION,
logErrors: false // 设置为true,错误不但会发给平台,也会在页面正常显示,方便调试(文档有)
})
同时在打包的时候,比如在测试环境,我不需要sourseMap,也不需要把代码上传到服务器(免得服务器上代码太乱),所以在webpack上也要最对应的修改:
if(process.env.NODE_ENV === 'sentry') {
const SentryCliPlugin = require('@sentry/webpack-plugin')
plugins.push(
new SentryCliPlugin({
include: './dist/', // 作用的文件夹,如果只想js报错就./dist/js
release: process.env.RELEASE_VERSION, // 一致的版本号
configFile: 'sentry.properties', // 不用改
ignore: ['node_modules', 'webpack.config.js'],
urlPrefix: '~/share/green/',// 去掉根目录的文件路径,不写找不到map文件
})
)
}
其中plugins为webpackConfig中的plugins,提成数组,不然没法动态加
同时将上文中开启sourcemap的地方都改为process.env.NODE_ENV === 'sentry'
每次打包后还有个问题,就是map文件不需要上传到服务器,需要删除,手动又太麻烦,有webpack的插件可以只打包js,剔除map,但是还要自己解压一边,很麻烦,于是写了个命令行,直接删除
新建记事本,写上下面代码后,后缀改为cmd即可
del /f /s /q .\dist\*.map
至此已经可以在线上使用了
# 搭建自己的sentry服务器
待续
# 搭建自己的前端监控
export default function errorHandler (Vue) {
const error = (e) => {
console.log(22222222, e, navigator.userAgent)
}
window.onerror = (msg, url, lineNo, columnNo, e) => {
if (msg === 'Script error.' || !url || e.type === '__skynet_wrapper__') return
error(e)
}
window.addEventListener('unhandledrejection', (e) => {
if (isObject(e.reason)) {
error(e.reason)
} else {
error(e.reason)
}
})
// const originAddEventListener = EventTarget.prototype.addEventListener
// EventTarget.prototype.addEventListener = (type, listener, options) => {
// const wrappedListener = (...args) => {
// try {
// return listener.apply(this, args)
// } catch (err) {
// throw err
// }
// }
// return originAddEventListener.call(this, type, wrappedListener, options)
// }
let vuePlugin = function (_Vue) {
if (!_Vue || !_Vue.config) {
return
}
const _oldOnError = _Vue.config.errorHandler
_Vue.config.errorHandler = function (errMsg, vm, info) {
error(errMsg)
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(errMsg)
}
if (typeof _oldOnError === 'function') {
_oldOnError.call(this, errMsg, vm, info)
}
}
}
vuePlugin(Vue)
const captureBreadcrumb = function (obj) {
console.log('obj', obj)
}
const _breadcrumbEventHandler = (evtName) => {
let _lastCapturedEvent = ''
return function (evt) {
if (_lastCapturedEvent === evt) {
return
}
_lastCapturedEvent = evt
let target
try {
target = htmlTreeAsString(evt.target)
} catch (e) {
target = '<unknown>'
}
captureBreadcrumb({
category: `ui.${evtName}`,
message: target
})
}
}
function htmlTreeAsString (target) {
console.log('target', target)
return target.outerHTML
}
window.addEventListener('click', _breadcrumbEventHandler('click', false))
// setTimeout(() => {
// const p = new Promise((resolve, reject) => reject('出错了'))
// p.then(null)
// }, 1000)
const nativeAjaxSend = XMLHttpRequest.prototype.send // 首先将原生的方法保存。
const nativeAjaxOpen = XMLHttpRequest.prototype.open
XMLHttpRequest.prototype.open = function (mothod, url, ...args) { // 劫持open方法,是为了拿到请求的url
const xhrInstance = this
xhrInstance._url = url
nativeAjaxOpen.apply(this, [mothod, url].concat(args))
}
XMLHttpRequest.prototype.send = function (...args) { // 对于ajax请求的监控,主要是在send方法里处理。
const oldCb = this.onreadystatechange
// const oldErrorCb = this.onerror
const xhrInstance = this
xhrInstance.addEventListener('error', function (e) { // 这里捕获到的error是一个ProgressEvent。e.target 的值为 XMLHttpRequest的实例。当网络错误(ajax并没有发出去)或者发生跨域的时候,会触发XMLHttpRequest的error, 此时,e.target.status 的值为:0,e.target.statusText 的值为:''
const errorObj = {
error_msg: 'ajax filed',
error_stack: JSON.stringify({
status: e.target.status,
statusText: e.target.statusText
}),
error_native: e
}
/* 接下来可以对errorObj进行统一处理 */
captureBreadcrumb({
type: 'http',
category: `xhr`,
message: errorObj
})
})
xhrInstance.addEventListener('abort', function (e) { // 主动取消ajax的情况需要标注,否则可能会产生误报
if (e.type === 'abort') {
xhrInstance._isAbort = true
}
})
this.onreadystatechange = function (...innerArgs) {
if (xhrInstance.readyState === 4) {
if (!xhrInstance._isAbort && xhrInstance.status !== 200) { // 请求不成功时,拿到错误信息
const errorObj = {
error_msg: JSON.stringify({
code: xhrInstance.status,
msg: xhrInstance.statusText,
url: xhrInstance._url
}),
error_stack: '',
error_native: xhrInstance
}
/* 接下来可以对errorObj进行统一处理 */
captureBreadcrumb({
type: 'http',
category: `xhr`,
message: errorObj
})
}
}
oldCb && oldCb.apply(this, innerArgs)
}
nativeAjaxSend.apply(this, args)
}
function _captureUrlChange (lastHref, currentHref) {
console.log('hrefChange', lastHref, currentHref)
}
const oldOnPopState = window.onpopstate
let lastHref = ''
window.onpopstate = function (...args) {
let currentHref = location.href
_captureUrlChange(lastHref, currentHref)
lastHref = currentHref
if (oldOnPopState) {
oldOnPopState.apply(this, args)
oldOnPopState.apply(this, args)
}
}
// const consoleList = ['info', 'warn', 'error']
// let wrapConsoleMethod = (level, callback) => {
// const nativeConsole = window.console[level]
// window.console[level] = (...args) => {
// callback && callback(level, args)
// nativeConsole.apply(this, args)
// }
// }
// let consoleMethodCallback = function (level, ...args) {
// captureBreadcrumb({
// message: args,
// level: level,
// category: 'console'
// })
// }
// consoleList.forEach((level) => {
// wrapConsoleMethod(level, consoleMethodCallback)
// })
}
function isObject (value) {
const type = typeof value
return value != null && (type === 'object' || type === 'function')
}
// function isFunction (value) {
// const type = typeof value
// return value != null && type === 'function'
// }