最簡單的上報
這裡講的是如何高效合理的捕捉與定位問題,不涉及 pv、uv、埋點之類的業務監控
首先我們要明白一點,前端如何捕獲錯誤,在程式碼中我們可以經常使用 try...catch
來捕獲錯誤,但是 try...catch
無法捕獲語法錯誤和非同步錯誤,如下
所以 try...catch
不適合做全域性的異常監聽,當然對於已知的可能會發生的錯誤,這個時候主動上報還是有用的
這個時候我們要想到在 window
上監聽 error
事件,監聽 error
事件可以返回相應的錯誤資訊、指令碼的url、行號、列號、error物件,如下(具體可參考developer.mozilla.org/zh-CN/docs/…):
window.onerror = (message, source, line, column, error) => {
console.log(message)
console.log(source)
console.log(line)
console.log(column)
console.log(error)
}
const a = { b: 2 }
console.log(a.c.length + 1)
複製程式碼
接下來就是上報異常資訊了,這裡當然可以通過 ajax 上報,這是肯定不會錯的,不過如果你細心點看下自己公司的上報頁面資料,包括監控我覺得大部分都會使用動態建立 img
標籤來上報的,這樣的好處在於不用處理跨域的問題,如下
const img = new Image()
img.url = `http://minitor.example.com?message=${msg}`
複製程式碼
關於 sourcemap
現在的前端基本上都會使用 webpack 打包js,按照上方的這種簡單的上報,上報的都是壓縮後的程式碼,那麼即使線上有錯誤看到這樣的訊息基本上也很難快速定位到問題,
生成 sourcemap 後的檔案會在底部顯示 sourcemap 的連結,如下:
瀏覽器就會根據這個連結去拉取程式碼的sourcemap,這樣就是可以使用sourcemap了,如果你的瀏覽器可以生成拉取到 sourcemap 那麼error 事件的error
物件中就可以看到相應的原始碼的行數,這樣也能快速定位問題,如下:
如何我們修改 sourcemap
的 url 地址,這樣導致瀏覽器拉取不到sourcemap,報錯資訊就會像下面這樣:
那麼這樣不就可以了嗎,我們只要把 sourcemap
也暴露出去就行了呀,這裡得區分下開發人員和使用者了,對於開發人員來說,當監控告訴我線上有問題的時候,我當然希望自己能夠浮現這個問題,這個時候暴露sourcemap給開發人員那當然是有助於及時定位問題的,但是對於普通使用者來說,首先沒法確認普通使用者的瀏覽器是否支援sourcemap並且開啟了sourcemap,還有一個問題就是有了sourcemap,就等於是把你的原始碼給了別人看,這是否合適是值得商榷的。
綜合以上兩點,得出結論,sourcemap 希望開發人員能夠得到,但是不希望普通使用者看到原始碼。
sourcemap
不希望使用者得到,這個很簡單隻要在使用者的網路環境不響應 sourcemap
的請求即可,這個可以在 Nginx 上配置,或者說可以放在內網中,這樣開發人員通過 VPN 也能直接線上上看到原始碼的問題,以 webpack 為例,將 sourcemap 放在不同域中,你需要關閉 devtool
中的 sourcemap,使用 source-map-dev-tool-plugin ,如下:
const webpack = require('webpack')
const UglifyJsPlugin = require ('uglifyjs-webpack-plugin');
module.exports = {
// other config
mode: 'production',
devtool: false,
optimization: {
noEmitOnErrors: true,
minimizer: [new UglifyJsPlugin({
sourceMap: true,
})
]
},
plugins: [
new webpack.SourceMapDevToolPlugin ({
publicPath: 'http://localhost:8002/',
filename: '[file].map',
})
]
}
複製程式碼
服務端解析資訊
接下來就是考慮監控系統了,監控系統是用來收集前端的異常資訊,並在達到一定閾值後向自動告訴開發人員(雖然這得讓開發人員出於 on call 狀態),上面我們說到了使用者上報過來的資訊是壓縮後的行、列號資訊,這樣的資訊本質上對於開發人員來說意義是不大的,因此需要在服務端將行列號解析一遍,這個工作看起來貌似沒法完成呀,不過感謝 mozilla 開源的 source-map,這可以讓這個工作變得異常簡單,只要讀取生成的 sourcemap,將行列號資訊作為引數傳遞即可
const map = fs.readFileSync(path.resolve(__dirname, 'sourcemaps', `dist/${source.split('/').pop()}.map`), 'utf8').toString()
const consumer = await new SourceMap.SourceMapConsumer(JSON.parse(map));
const result = consumer.originalPositionFor({
line,
column,
})
複製程式碼
一個包含簡單的解析的微型監控系統如下:
const Koa = require('koa')
const router = require('koa-router')()
const SourceMap = require('source-map')
const app = new Koa()
const path = require('path')
const fs = require('fs')
const chalk = require('chalk')
router.get('/log.gif', async (ctx, next) => {
const source = ctx.query.source
const line = +ctx.query.l
const column = +ctx.query.c
const msg = ctx.query.msg
const err = ctx.query.err
const map = fs.readFileSync(path.resolve(__dirname, 'sourcemaps', `dist/${source.split('/').pop()}.map`), 'utf8').toString()
const consumer = await new SourceMap.SourceMapConsumer(JSON.parse(map));
const result = consumer.originalPositionFor({
line,
column,
})
console.log(chalk.red('原始上報的指令碼異常資訊:'), '\n')
console.log(chalk.red(`行號:${line}`), '\n')
console.log(chalk.red(`列號:${column}`), '\n')
console.log(chalk.red(`檔案:${source}`), '\n')
console.log(chalk.red(`資訊:${msg}`), '\n')
console.log(chalk.red(`error物件:${err}`), '\n')
console.log(chalk.green(`解析後原始碼對應的資訊:`), '\n')
console.log(chalk.green(`行號:${result.line}`), '\n')
console.log(chalk.green(`列號:${result.column}`), '\n')
console.log(chalk.green(`檔案:${result.source}`), '\n')
console.log(chalk.green(`name:${result.name}`), '\n')
console.log(result)
consumer.destroy();
ctx.body = ''
})
app
.use(router.routes())
.use(router.allowedMethods())
app.listen('8002', () => {
console.log('monitro server is listening port 8002')
})
複製程式碼
我們執行一遍,如下:
這樣我們就完成了我們的一個簡單的獲取原始資訊的簡單監控系統。
關於如何獲取原始程式碼的堆疊資訊,在上報資訊、解析資訊的進階知識,歡迎期待下一篇文章。
整個系統的程式碼你可以在這個倉庫找到:github.com/huruji/mini…