如何调试线上前端错误并快速定位用户问题?
线上前端错误无法在本地复现时,正确做法是「先在浏览器全局钩子捕获 → 带上 release 版本与 Source Map 还原栈 → 用用户标识和操作轨迹把报错还原成一条可回放的路径」,四步走完,90% 的用户报错能在 10 分钟内定位到具体代码行和触发条件。
线上前端错误调试的标准流程是什么?
结论:报错定位必须闭环成「捕获 → 上报 → 还原 → 关联用户」四步,缺任何一步都会变成「用户说打不开,你猜」。
第一步捕获:在应用入口最早期(早于任何框架初始化)注册全局错误钩子,保证框架自身报错也能被接住。第二步上报:把错误栈、release 版本号、用户 ID、当前路由、UA、时间戳一起发出,推荐用 navigator.sendBeacon('/log/error', blob),它在页面卸载时不会被中断,比 fetch 可靠。第三步还原:用 Source Map 把压缩后的 a.b.c:1:48213 映射回 src/pages/order/index.tsx:88:12。第四步关联:用同一个 requestId 串起前端报错与后端日志,否则你只能看到「接口 500」,看不到用户点了哪个按钮。
这四步的产物是一个可检索的报错详情页,而不是一堆 console 文本。
window.onerror 和 unhandledrejection 分别管什么?
结论:同步错误和资源加载错误走 window.addEventListener('error', handler, true),Promise 未捕获走 unhandledrejection,两者必须同时注册。
window.addEventListener('error', (e) => {
// 资源错误(img/script/link)没有 e.error,只有 e.target
if (!e.error) return report({ type: 'resource', url: e.target?.src || e.target?.href });
report({ type: 'js', msg: e.message, stack: e.error.stack, file: e.filename, line: e.lineno, col: e.colno });
}, true); // 第三个参数 true 才捕获资源错误,默认 false 漏掉
window.addEventListener('unhandledrejection', (e) => {
report({ type: 'promise', reason: e.reason?.stack || String(e.reason) });
});
注意点有两个:一是 error 事件不加 true(捕获阶段)就抓不到 <img>、<script> 加载失败;二是 Vue 3 用 app.config.errorHandler、React 用 componentDidCatch 补一层组件内错误,因为被框架内部 catch 的错误不会冒泡到 window。Safari 对 unhandledrejection 支持较晚,iOS 13 以下需要在业务代码里手动 .catch()。
Source Map 怎么配才能还原线上报错源码?
结论:生产构建输出 hidden-source-map,把 .map 文件上传到错误监控平台或私有静态目录,绝不要随 JS 一起发布到 CDN。
webpack 5 配置:
devtool: 'hidden-source-map', // 生成 map 但不写 //# sourceMappingURL 注释
output: { filename: '[name].[contenthash:8].js' }
hidden-source-map 的作用是:map 文件照样生成,浏览器 DevTools 不会自动加载它,用户看不到源码,但你的监控平台用 release 版本号可以拉取它做还原。上传时给每个 release 打 git rev-parse --short HEAD 作为版本号,Sentry 的 sentry-cli releases files <version> upload-sourcemaps ./dist 就是这个用法。
还有一个必踩的坑:脚本跨域加载时,浏览器会把错误信息统一变成 Script error.,栈信息全部丢失。解决办法是在 <script> 标签上加 crossorigin="anonymous",并让 CDN 返回 Access-Control-Allow-Origin: *,两者缺一不可。
怎么把报错和具体用户、具体操作关联起来?
结论:给每个访问会话生成一个 UUID 作为 sessionId,在每次上报里携带「最近 20 条用户操作 breadcrumb」,报错就能还原成一条路径。
具体做法:用户进入页面时生成 sessionId(crypto.randomUUID()),登录后补上 userId。在路由切换、按钮点击、接口请求三个位置埋 breadcrumb,只记录「时间 + 类型 + 简短描述」,例如 {t: 1690000000, type: 'click', text: '提交订单'}。每条错误上报携带最近 20 条,数组序列化后一般小于 2KB,sendBeacon 完全承受得住。
配合接口层统一加 X-Request-Id(前端生成,后端原样写进日志),用户报错时可以拿这个 ID 直接去后端日志里捞对应的请求记录。这比让用户截图有效得多——截图看不出接口返回了什么。
采样率方面:错误上报建议全量(错误量本身有限),性能数据才做 10% 采样,因为性能日志动辄每页几十条。
用户只说「页面白屏了」时怎么快速定位?
结论:白屏优先看三件事:JS 加载失败、首屏渲染异常、资源 404;用「错误列表按 URL 聚合 + 按版本对比」两个维度筛选,能直接定位到是哪次发布引入的。
查看顺序:先看错误详情里的 file 是否 404,再看该错误首次出现时间是否与某次发版时间吻合。监控平台一般支持「按 release 分组」,把昨天版本和今天版本的错误数一对比,新增的报错类型就是本次发版引入的,直接回滚或修复对应提交。
复现手段上,Chrome DevTools 的 Network 面板开「Slow 3G + CPU 4x slowdown」可以模拟低端机,白屏错误里 70% 来自接口超时后没写 loading 兜底,而不是代码 bug。给异步渲染加一层 Suspense 或骨架屏,能消掉一大类「白屏」投诉。
最后提醒一句:console.error 在生产不要屏蔽,它会影响上报的堆栈深度;同时把 Error.stackTraceLimit 保持在默认的 10 层以上,栈太浅根本看不到业务调用链。
总结一下:全局钩子(含 true 捕获参数)抓错误,hidden-source-map + crossorigin 还原栈,sessionId + breadcrumb + X-Request-Id 关联用户与请求,按 release 对比定位是哪次发布引入的问题——这四件事做完,线上前端问题就从「靠猜」变成「靠查」。
原文链接:https://www.gj0.com/thread-296.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。