{"action":"created","issue":{"url":"https://api.github.com/repos/wayou/wayou.github.io/issues/8","repository_url":"https://api.github.com/repos/wayou/wayou.github.io","labels_url":"https://api.github.com/repos/wayou/wayou.github.io/issues/8/labels{/name}","comments_url":"https://api.github.com/repos/wayou/wayou.github.io/issues/8/comments","events_url":"https://api.github.com/repos/wayou/wayou.github.io/issues/8/events","html_url":"https://github.com/wayou/wayou.github.io/issues/8","id":278664062,"number":8,"title":"前端异常监控","user":{"login":"wayou","id":3783096,"avatar_url":"https://avatars0.githubusercontent.com/u/3783096?v=4","gravatar_id":"","url":"https://api.github.com/users/wayou","html_url":"https://github.com/wayou","followers_url":"https://api.github.com/users/wayou/followers","following_url":"https://api.github.com/users/wayou/following{/other_user}","gists_url":"https://api.github.com/users/wayou/gists{/gist_id}","starred_url":"https://api.github.com/users/wayou/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wayou/subscriptions","organizations_url":"https://api.github.com/users/wayou/orgs","repos_url":"https://api.github.com/users/wayou/repos","events_url":"https://api.github.com/users/wayou/events{/privacy}","received_events_url":"https://api.github.com/users/wayou/received_events","type":"User","site_admin":false},"labels":[{"id":762351421,"url":"https://api.github.com/repos/wayou/wayou.github.io/labels/javascript","name":"javascript","color":"fbca04","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":1,"created_at":"2017-12-02T07:35:50Z","updated_at":"2018-05-09T06:13:54Z","closed_at":null,"author_association":"OWNER","body":"## 前端异常监控\r\n\r\n如果说前端的异常监控有个救星的话，我想那就是 `window.onerror` 这个全局错误监听事件了。它给了我们统一处理前端全局错误的机会，使得错误上报有了一线生机。\r\n\r\n```js\r\nwindow.onerror = function(messageOrEvent, source, lineno, colno, error) { ... }\r\n```\r\n\r\n* message: 错误信息，在 HTML 中的 `onerror` 属性中设置的回调可以传递事件\r\n* source: 出错文件的 url\r\n* lineno: 出错位置的行数\r\n* colno: 出错时的列数\r\n* error: 出错时的 Error 对象。\r\n\r\n实践中发现最后个参数 Error 对象中的值因浏览器的实现各有差异，比如 Chrome 中包含 `message` 和 `stack`，而 Safari 中则包含了前面四个参数的所有值。这在下面的示例代码的结果中可以看得出来。\r\n\r\n### 牵出来溜一溜\r\n\r\n*注意*\r\n`window.onerror` 需要在有服务端的情况下才能正常工作，本地直接打开页面测试获取不到任何有用的错误信息。可以在命令行启动一个简单的服务端来进行测试。\r\n因为 Mac 自带 Python，一般需要用到服务端的时候，我喜欢用 Python 自带的 `SimpleHTTPServer`：\r\n\r\n```bash\r\npython -m SimpleHTTPServer\r\nor\r\npython3 -m http.server\r\n```\r\n\r\n以下代码我们对全局错误进行监听，然后将错误打印到页面：\r\n\r\n```js\r\nwindow.onerror = function (msg, source, line, col, error) {\r\n    printError.apply(null, arguments);\r\n};\r\n\r\nfunction printError(msg, source, line, col, error) {\r\n    var detail =\r\n        'msg:' +\r\n        msg +\r\n        '\\ncourse:' +\r\n        source +\r\n        '\\nline:' +\r\n        line +\r\n        '\\ncol:' +\r\n        col +\r\n        '\\nerror:' +\r\n        JSON.stringify(error, Object.getOwnPropertyNames(error));\r\n    var div = document.createElement('pre');\r\n    div.innerHTML = detail\r\n    document.body.appendChild(div);\r\n}\r\n```\r\n\r\n然后在页面放上按钮以触发错误。这里测试了两种错误，一种运行时 JS 的抛错，另一种手动在代码中抛出的错误。\r\n\r\n```html\r\n<button onclick=\"excptionGenerate()\">点我执行出错代码</button>\r\n<button onclick=\"throwError()\">点我手动抛出异常</button>\r\n```\r\n\r\n![Chrome 中异常的捕获与打印](https://raw.githubusercontent.com/wayou/wayou.github.io/master/posts/js-exception-report/assets/chrome-error-print.png)\r\n\r\n_Chrome 中异常的捕获与打印_\r\n\r\n\r\n![Safari 中异常的捕获与打印](https://raw.githubusercontent.com/wayou/wayou.github.io/master/posts/js-exception-report/assets/safari-error-print.png)\r\n\r\n_Safari 中异常的捕获与打印_\r\n\r\n\r\n### 浏览器兼容性\r\n\r\n要知道，最初版本的全局错误监听事件是这样的：\r\n\r\n```js\r\nwindow.onerror = function(messageOrEvent, source, lineno) { ... }\r\n```\r\n\r\n后来才增加了 `colno` 和 `error`。而后来加的这两个参数其实是非常有用的。\r\n\r\n因为线上代码一般为压缩过的代码，所有内容都在一行，假如没有提供发生问题的列数，这样的错误日志要追查起来很不方便。\r\n错误对象则直接提供了错误堆栈信息（通过 `error.stack` 访问），就像我们在浏览器控制台看到的一样，对于定位问题十分有帮助。\r\n\r\n主流浏览器中， Chrome， Safari 已经完成了5个参数的支持。\r\n\r\nFirefox 从 31 开始支持了完整的5个参数。\r\n\r\n截止到目前， 微软的 Edge 浏览器还没有实现对新增两个参数的支持。其实现情况可以在[这里](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/runtimeerrorreportingerrorevent/?q=onerror)查阅得到。\r\n\r\n* 小贴士 *\r\n> 过程中顺便发现了微软Edge[这个API Catalog页面](https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?page=1&q=queryselector)可以查到主流浏览器对名前端特性的实现情况，数据比 caniuse 全，譬如 `window.onerror` 在 caniuse 上则没有。\r\n\r\n![MS Edge 浏览器对 `window.onerror` 第五个参数的实现情况](https://raw.githubusercontent.com/wayou/wayou.github.io/master/posts/js-exception-report/assets/ms-edge-onerror-compatability.png)\r\n\r\n_MS Edge 浏览器对 `window.onerror` 第五个参数的实现情况_\r\n\r\n从这里也可以看到，其他主流浏览器都已经有了完整的支持。\r\n\r\nIE，（逃~）\r\n\r\n### 垫片\r\n\r\n对于不提供第5个参数的环境，我们是拿不到错误堆栈信息的。这种情况下对错误的追查帮助不大。\r\n\r\n但是，手动在代码中捕获并抛出的错误，是带了堆栈信息的。这就有了补救的希望。我们可以将可能出错的地方，或者我们期望进行监控的地方，使用 try catch。\r\n\r\n```js\r\nfunction tryCatchError() {\r\n    try {\r\n        a();\r\n    } catch (error) {\r\n        printError(error)\r\n    }\r\n}\r\n\r\nfunction printError(error) {\r\n    var detail = 'error:' +\r\n            JSON.stringify(error, Object.getOwnPropertyNames(error)) +\r\n            '\\n\\n';\r\n    }\r\n\r\n    var div = document.createElement('code');\r\n    div.innerHTML = detail\r\n    document.body.appendChild(div);\r\n}\r\n\r\n```\r\n\r\n同时在页面中添加按钮来调用新的测试函数。\r\n\r\n```html\r\n<button onclick=\"tryCatchError()\">利用 try catch 捕获异常并打印错误堆栈</button>\r\n```\r\n\r\n![try catch 中打印错误](https://raw.githubusercontent.com/wayou/wayou.github.io/master/posts/js-exception-report/assets/try-catch-print.png)\r\n\r\n_try catch 中打印错误_\r\n\r\n我们看到，这种方式确实能得到详细的报错堆栈。\r\n\r\n\r\n#### 这一段其实无关紧要\r\n\r\n因为最后两个参数是后面加的，有理由相信，在很老很老很老的 Chrome 版本中，也是不支持全部5个参数的。来自Ben Vinegar的[这篇文章](https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror.html)指出 Chrome 46 开始支持全部5个参数的。\r\n\r\n![](https://raw.githubusercontent.com/wayou/wayou.github.io/master/posts/js-exception-report/assets/chrome-onerr-compatability.png)\r\n\r\n为此我们不妨找一个老版本来验一下。我去 [Chrome 的历史仓库中](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html) 下到了 [Chromium 15](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Mac/100293/) （Chrome 正式发布前的开发版）。\r\n\r\n![拥有历史厚重感的老版本 Chrome](https://raw.githubusercontent.com/wayou/wayou.github.io/master/posts/js-exception-report/assets/chromium-15.png)\r\n\r\n_拥有历史厚重感的老版本 Chrome_\r\n\r\n打开它，仿佛打开了一个刚出土的文物，在 Retina 屏上，它的皮肤已经略出了清晰的锯齿，这是历史的厚重，这是岁月刻下的痕迹。\r\n\r\n不过还好，它的功能依然完好，跑起来我们的示例代码来驾轻就熟。\r\n\r\n![老版本的 Chrome 果然是没有后两位参数的](https://raw.githubusercontent.com/wayou/wayou.github.io/master/posts/js-exception-report/assets/chrome-15-without-last-2-arguments.png)\r\n\r\n_老版本的 Chrome 果然是没有后两位参数的_\r\n\r\n\r\n### 全局无法捕获的情况\r\n\r\n除了考虑上面的浏览器兼容性问题外，还有其他一些情况，也是无法通过这个全局的 onerror 获取到详细报错信息的。\r\n\r\n#### 跨域情况的错误捕获(CDN)\r\n\r\n`window.onerror` 有个限制，来自非同域的代码有报错，不会给出错误的详细信息，只能得到一个 `Script error.`。这是浏览器出于安全考虑，不向第三方泄露信息而做的一个措施。但往往线上代码大部分都部署在 CDN，所以这个限制的影响还挺常见。\r\n\r\n不过还好，某些浏览器中可以通过配置来更改这一行为，让我们能正常拿到报错的详细信息。\r\n\r\n还有一点，就是虽然在 `window.onerror` 中倒不到详细的报错信息，但在浏览器控制台是可以看到详细信息的。\r\n\r\n*如果是跨域脚本，则提示去控制台查看报错信息*\r\n```js\r\nwindow.onerror = function (msg, url, lineNo, columnNo, error) {\r\n    var string = msg.toLowerCase();\r\n    var substring = \"script error\";\r\n    if (string.indexOf(substring) > -1){\r\n        alert('Script Error: See Browser Console for Detail');\r\n    } else {\r\n        var message = [\r\n            'Message: ' + msg,\r\n            'URL: ' + url,\r\n            'Line: ' + lineNo,\r\n            'Column: ' + columnNo,\r\n            'Error object: ' + JSON.stringify(error)\r\n        ].join(' - ');\r\n\r\n        alert(message);\r\n    }\r\n\r\n    return false;\r\n};\r\n```\r\n\r\n控制台能看到对于线上的错误监控来说没多大用，还是得解决上报的问题。我们来看看如何设置跨域脚本让我们可以捕获时拿到错误堆栈信息。\r\n\r\n下面看跨域脚本的配置。\r\n\r\n* CDN 上开启允许跨域  \r\n```\r\nAccess-Control-Allow-Origin:*\r\n或者\r\nAccess-Control-Allow-Origin: domain of your site\r\n```\r\n\r\n* 然后 script 标签上设置跨域标识为匿名\r\n\r\n```html\r\n<script crossorigin=\"anonymous\" src=\"//url/for/your/cdn/scripts\"></script>\r\n```\r\n\r\n唯一需要注意的是，一旦在前端设置了 `crossorigin`，要确保服务端相应设置了允许跨域的响应头，否则整个脚本文件会加载失败，影响页面正常功能。\r\n\r\n目前来看，除了 Opera外，各主流浏览器都有支持此属性。\r\n\r\n![跨域脚本加载浏览器兼容性](https://raw.githubusercontent.com/wayou/wayou.github.io/master/posts/js-exception-report/assets/cors-loading-browser-compatability.png)\r\n\r\n_跨域脚本加载浏览器兼容性_\r\n\r\n\r\n#### iframe 中异常的捕获\r\n\r\niframe 中发生异常，外界的 `onerror` 是不会触发的。但如果 iframe 地址同域，那么我们就可以设置 iframe 的全局 `onerror` 进行监听。\r\n\r\n```js\r\ndocument.getElementById(\"myiframe\").contentWindow.onerror=function() {\r\n    alert('error!!');\r\n    return false;\r\n}\r\n``` \r\n以上代码需要保证在 iframe 加载完成后进行。\r\n\r\n非同域情况下，如果 ifame 内的内容不来自第三方，也就是你自己可以控制，那么可以通过与 iframe 内进行通信的方式，将异常信息抛出来。iframe 通信试有很多，譬如 `postMessage`。这里不展开了。\r\n\r\n非同域且内容不受自己控制的情况下，除了在控制台查看错误详细信息，真的没其他办法可以捕获了。\r\n\r\n\r\n#### 代码压缩在错误捕获中的还原\r\n\r\n线上代码一般是压缩过的，如何更友好地展示还原事件发生地，对于错误上报也是个挑战。因为在错误监听的回调里面提供了列数，所以对于压缩后的代码，定位起位置来也不是难事，再结合错误对象里的报错堆栈信息，能够很好地定位代码的位置及原因。\r\n\r\n关于压缩后的代码，有 sourse map 可以映射到源码，如果我们在异常捕获时通过与 soruce map 文件结合起来，那么就有可能在还原错误时分析出其在未压缩源码中的位置。目前来看，已经有相关的服务实现了这一功能，[sentry 的文档](https://docs.sentry.io/clients/javascript/sourcemaps/)中有提到。\r\n\r\n前面讨论了一下错误详细信息的获取，最终我们是想在客户端收集到这些信息，再加上 ua, 发生错误的时间，设备相关信息等上报到服务端。总之，能够获取到信息越全越好，方便我们后期定位处理问题。当然，这套体系，市面上是有现成库已经做了的，来自 GitHub的这个代码仓库[cheeaun/javascript-error-logging](https://github.com/cheeaun/javascript-error-logging)收集了前端异常监控相关的资源，从 [GitHub 这个专题页面](https://github.com/topics/error-monitoring)也能找到许多。上面的坑库里面也都会覆盖到。 \r\n\r\nHappy trouble shotting :)\r\n\r\n\r\n### 相关资料\r\n\r\n* [GlobalEventHandlers.onerror](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror)\r\n* [How to catch JavaScript Errors with window.onerror (even on Chrome and Firefox)](https://danlimerick.wordpress.com/2014/01/18/how-to-catch-javascript-errors-with-window-onerror-even-on-chrome-and-firefox/)\r\n* [Cryptic “Script Error.” reported in Javascript in Chrome and Firefox](https://stackoverflow.com/questions/5913978/cryptic-script-error-reported-in-javascript-in-chrome-and-firefox)\r\n* [Capture and report JavaScript errors with window.onerror](https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror.html)\r\n* [MS Edge API Catalog](https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?page=1&q=queryselector)\r\n* [Script Error: JavaScript Forensics](https://trackjs.com/blog/script-error-javascript-forensics/)\r\n* [CORS settings attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes)\r\n* [can I catch exception of Iframe in parent window of Iframe](https://stackoverflow.com/questions/6327128/can-i-catch-exception-of-iframe-in-parent-window-of-iframe)\r\n\r\n\r\n**后记**\r\n\r\n后续可以开垦的点：\r\n\r\n- 本地模拟跨域\r\n- 自己实现异常上报的库\r\n"},"comment":{"url":"https://api.github.com/repos/wayou/wayou.github.io/issues/comments/387631465","html_url":"https://github.com/wayou/wayou.github.io/issues/8#issuecomment-387631465","issue_url":"https://api.github.com/repos/wayou/wayou.github.io/issues/8","id":387631465,"user":{"login":"Yalhu","id":27722787,"avatar_url":"https://avatars1.githubusercontent.com/u/27722787?v=4","gravatar_id":"","url":"https://api.github.com/users/Yalhu","html_url":"https://github.com/Yalhu","followers_url":"https://api.github.com/users/Yalhu/followers","following_url":"https://api.github.com/users/Yalhu/following{/other_user}","gists_url":"https://api.github.com/users/Yalhu/gists{/gist_id}","starred_url":"https://api.github.com/users/Yalhu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Yalhu/subscriptions","organizations_url":"https://api.github.com/users/Yalhu/orgs","repos_url":"https://api.github.com/users/Yalhu/repos","events_url":"https://api.github.com/users/Yalhu/events{/privacy}","received_events_url":"https://api.github.com/users/Yalhu/received_events","type":"User","site_admin":false},"created_at":"2018-05-09T06:13:54Z","updated_at":"2018-05-09T06:13:54Z","author_association":"NONE","body":"top页面中有iframe，跨域。iframe中引用的外部脚步（和iframe同源）报错，在iframe和top页面中都没有监听到错误啊？\r\n但是在控制台，可以看到错误输出。"}}
{"actor":{"display_login":"Yalhu"}}