Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于前端脚本异常监控的思考 #63

Open
huruji opened this issue Apr 18, 2019 · 1 comment
Open

关于前端脚本异常监控的思考 #63

huruji opened this issue Apr 18, 2019 · 1 comment

Comments

@huruji
Copy link
Owner

huruji commented Apr 18, 2019


关于前端脚本异常监控的思考

最简单的上报

这里讲的是如何高效合理的捕捉与定位问题,不涉及 pv、uv、埋点之类的业务监控

首先我们要明白一点,前端如何捕获错误,在代码中我们可以经常使用 try...catch 来捕获错误,但是 try...catch 无法捕获语法错误和异步错误,如下

所以 try...catch 不适合做全局的异常监听,当然对于已知的可能会发生的错误,这个时候主动上报还是有用的

这个时候我们要想到在 window 上监听 error 事件,监听 error 事件可以返回相应的错误信息、脚本的url、行号、列号、error对象,如下(具体可参考https://developer.mozilla.org/zh-CN/docs/Web/API/GlobalEventHandlers/onerror):

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')
})

我们运行一遍,如下:

这样我们就完成了我们的一个简单的获取原始信息的简单监控系统。

关于如何获取原始代码的堆栈信息,在上报信息、解析信息的进阶知识,欢迎期待下一篇文章。

整个系统的代码你可以在这个仓库找到:https://github.com/huruji/mini-web-monitor

@yanyang1116
Copy link

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants