线上报错监控接进 Sentry 之后一直有个头疼的问题:报错堆栈全是压缩混淆之后的代码,a.js:1:23456 这种,压根看不出是哪个业务文件哪一行出的问题。折腾了好一阵子 source map 的配置才算把这块理顺,中间踩的坑记一下。
最开始犯的错是 devtool 配置在开发和生产环境用了同一套,图省事:
1 | module.exports = { |
这个配置在开发环境刷新快、调试体验好,但线上构建也用它的话,打出来的产物体积会明显偏大,而且 eval 相关的 source map 类型本质上是把映射信息内联在 eval 语句里,用于生产环境既不合适也没必要。生产环境该用的是 source-map(单独文件,映射最精确)或者 hidden-source-map:
1 | module.exports = { |
hidden-source-map 会生成 .map 文件但不在 JS 文件末尾加那行 //# sourceMappingURL=,这样浏览器不会尝试自动加载 map 文件(用户打开控制台看到的还是压缩代码,不会额外发请求下载 map),但你可以把这个 .map 文件单独上传给 Sentry 这类错误监控平台,报错的时候由平台在后台做符号还原,线上用户完全不会感知到 map 文件的存在。
第二个坑是把生成出来的 .map 文件直接和 JS 一起发布到了 CDN 上,图方便没多想,结果等于把没压缩过的原始源码结构、变量名、文件路径全都暴露给了任何打开浏览器网络面板的人。虽然大部分场景不算什么严重的安全问题,但公司代码里有些内部接口路径、注释掉的调试代码,确实不太适合公开给所有人看。后来把发布流程改成两步:JS 产物正常发到 CDN,.map 文件单独上传到 Sentry(或者自己的符号服务器)之后就从构建产物目录里删掉,不进最终发布的静态资源,两边分开走。
Sentry 这边接入的时候还遇到一个对不上号的问题,上传了 source map,报错平台上堆栈还是显示编译后的代码,查了半天是版本号没对齐。Sentry 是靠 release 版本号加上文件路径去匹配 source map 和线上运行的 JS 文件的,构建的时候用 webpack 插件上传 map 时候设的 release,跟前端页面里 Sentry SDK 初始化时候用的 release 对不上(一个是构建时间戳,一个是 git commit hash,两边各写各的),匹配不上自然找不到对应的 map:
1 | // 构建脚本和前端初始化必须用同一个来源生成 release 号,比如都用 git commit hash |
1 | Sentry.init({ |
这个问题排查起来最烦的地方是它不报错,你会看到 map 确实上传成功了,日志也没有任何异常提示,就是报错堆栈死活对不上,得去 Sentry 后台的 release 列表里手动核对两边的版本号才能发现是这里出的岔子。
还有一个容易被忽略的地方是 source map 里对第三方依赖的映射,node_modules 里的代码默认也会被打进 map 里,如果项目里依赖比较多,.map 文件体积会比想象中大很多,上传和存储都有额外开销。可以通过 devtoolModuleFilenameTemplate 或者一些 loader 层面的配置把第三方依赖排除在详细映射之外,报错堆栈里第三方库那一层显示成压缩后的代码也无所谓,反正业务代码才是真正需要精确定位的部分。
折腾这一圈之后现在的流程基本是:本地开发用轻量的 eval 类型图快,CI 构建生产包用 hidden-source-map,map 文件不进 CDN 发布目录,单独上传给 Sentry 且 release 号在构建脚本和前端初始化里统一用 git commit hash 保证对得上。整个 source map 这套东西说复杂也不复杂,就是环节比想象中多一点,随便漏一环(该用哪种 devtool、要不要发布 map、release 号对不对齐)都会导致最后报错堆栈看不懂,想让它真正好用,每个环节都得对上。