關於前端指令碼異常監控的思考

灰風GreyWind發表於2019-04-20

最簡單的上報

這裡講的是如何高效合理的捕捉與定位問題,不涉及 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…

相關文章