├── .gitignore ├── README.md ├── __tests__ ├── fixtures │ ├── captured-errors.js │ └── tracekit-resource-spec.js ├── tracekit-computestacktrace-spec.js ├── tracekit-handler-spec.js ├── tracekit-parser-spec.js └── tracekit-spec.js ├── dist ├── errorWatch.esm.js ├── errorWatch.js ├── errorWatch.min.js └── errorWatch.min.js.map ├── package-lock.json ├── package.json ├── ref ├── supplement.js └── tracekit.js ├── rollup.config.js └── src ├── .babelrc ├── computeStackTrace.js ├── config.js ├── index.js ├── report.js ├── resourceError.js ├── tryCatch.js ├── utils.js └── wrap.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | *.DS_Store 4 | *.idea 5 | *.vscode 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前端错误解析 2 | 3 | 根据 [TraceKit](https://github.com/csnover/TraceKit) 改造。 4 | 改动点: 5 | 6 | 1. 使用 `es6` 对源文件进行改写,根据功能拆分成小文件便于维护; 7 | 1. 使用 `rollup` ,方便打成 `UMD`、`ES` 包,压缩代码; 8 | 1. 增加资源加载错误上报; 9 | 1. 测试套件由 `jasmine` 改成了 `Jest`。 10 | 11 | ## 安装 12 | 13 | ```bash 14 | npm i error-watch 15 | ``` 16 | 17 | ## 使用 18 | 19 | - es6 20 | 21 | ```javaScript 22 | import ErrorWatch from 'error-watch'; 23 | 24 | /** 25 | * 错误监控回调函数 26 | * @param stack {Object|null} 依次根据 Error 对象属性 stacktrace、stack、message和调用链 callers 来解析出错误行列等信息 27 | * @param isWindowError {Boolean} 是否触发 window 监听事件,手动 try/catch 获取 err 解析为 false 28 | * @param error {Error|null} 原始 err 29 | */ 30 | function receiveError(stack, isWindowError, error) { 31 | const data = encodeURIComponent(JSON.stringify({ 32 | ...stack, // 错误解析的对象 33 | // isWindowError 34 | url: window.location.href, // 报错页面 35 | })); 36 | // img 上报 37 | // 注意分析数据有可能过大,需要考虑 ajax? 38 | new Image().src = 'https://your-websize.com/api/handleError?data=' + data; 39 | } 40 | 41 | // 监听错误 42 | ErrorWatch.report.subscribe(receiveError); 43 | ``` 44 | 45 | - 脚本直接引入 46 | 47 | ```html 48 | 49 | 50 | ErrorWatch.report.subscribe(function() { 51 | // code here 52 | }); 53 | 54 | ``` 55 | 56 | ### 错误回调处理函数,传入三个参数 57 | 58 | - stack,成功是个 `Object` 否则是 `null`,可以用来结合 `SourceMap` 定位错误。 59 | ```json 60 | { 61 | "mode": "stack", 62 | "name": "ReferenceError", 63 | "message": "thisIsAbug is not defined", 64 | "stack": [ 65 | { 66 | "url": "http://localhost:7001/public/js/traceKit.min.js", 67 | "func": "Object.makeError", 68 | "args": [], 69 | "line": 1, 70 | "column": 9435, 71 | "context": null 72 | }, 73 | { 74 | "url": "http://localhost:7001/public/demo.html", 75 | "func": "?", 76 | "args": [], 77 | "line": 49, 78 | "column": 12, 79 | "context": null 80 | } 81 | ] 82 | } 83 | ``` 84 | 85 | - isWindowError,可选上报,区分自动还是手动。由于 try/catch 吐出来的 error 信息丰富,对于定位错误帮助较大,可以为业务逻辑自定义错误。 86 | ```javascript 87 | try { 88 | /* 89 | * your code 90 | */ 91 | throw new Error('oops'); 92 | } catch (e) { 93 | ErrorWatch.report(e); 94 | } 95 | ``` 96 | 97 | - error,原始错误对象,上述的 stack 如果内部解析成功,则例如 stack.stack 已翻译成数组,会抛弃原始的 stack。 98 | 如果需要可以这么做。 99 | 100 | ```javascript 101 | { 102 | ...stack, 103 | errorStack: error && error.stack, 104 | } 105 | ``` 106 | 107 | - 完整实例 108 | 109 | ```json 110 | { 111 | "mode": "stack", 112 | "name": "ReferenceError", 113 | "message": "thisIsAbug is not defined", 114 | "stack": [ 115 | { 116 | "url": "http://localhost:7001/public/js/traceKit.min.js", 117 | "func": "Object.makeError", 118 | "args": [], 119 | "line": 1, 120 | "column": 9435, 121 | "context": null 122 | }, 123 | { 124 | "url": "http://localhost:7001/public/demo.html", 125 | "func": "?", 126 | "args": [], 127 | "line": 49, 128 | "column": 12, 129 | "context": null 130 | } 131 | ], 132 | "errorStack": "ReferenceError: thisIsAbug is not defined\n at Object.makeError (http://localhost:7001/public/js/traceKit.min.js:1:9435)\n at http://localhost:7001/public/demo.html:49:12", 133 | "url": "http://localhost:7001/public/demo.html" 134 | } 135 | ``` 136 | 137 | ### 资源加载错误上报信息 138 | - stack,根据 `mode` 是 `resource`,来区分资源加载错误。 139 | ````json 140 | { 141 | "message": "img is load error", 142 | "mode": "resource", 143 | "name": "http://domain/404.jpg", 144 | "stack": null, 145 | "url": "http://localhost:7001/public/demo.html" 146 | } 147 | ```` 148 | ### 建议 149 | - 尽量不用匿名函数,都给它加个名字,便于错误定位。 150 | ```javascript 151 | Api.foo = function Api_foo() { 152 | }; 153 | const bar = function barFn() { 154 | }; 155 | ``` 156 | - Script error. 跨域脚本无法拿到错误信息。 157 | 1. 跨源资源共享机制 `CORS` :`Access-Control-Allow-Origin: Your-allow-origin` 158 | 1. 脚本属性 `crossOrigin` :`` 159 | 160 | ## npm scripts 161 | 162 | - `npm run build` 根据 `rollup.config.js` 配置文件进行打包。 163 | - `npm test` 单元测试。 164 | 165 | ## 阅读源码 166 | 167 | 阅读源码前,可以参考下关于JS错误知识的一些讨论 [错误监控原理分析](https://github.com/Godiswill/blog/issues/7)。 168 | -------------------------------------------------------------------------------- /__tests__/fixtures/captured-errors.js: -------------------------------------------------------------------------------- 1 | /* exported CapturedExceptions */ 2 | const CapturedExceptions = {}; 3 | 4 | CapturedExceptions.OPERA_854 = { 5 | message: "Statement on line 44: Type mismatch (usually a non-object value used where an object is required)\n" + 6 | "Backtrace:\n" + 7 | " Line 44 of linked script http://path/to/file.js\n" + 8 | " this.undef();\n" + 9 | " Line 31 of linked script http://path/to/file.js\n" + 10 | " ex = ex || this.createException();\n" + 11 | " Line 18 of linked script http://path/to/file.js\n" + 12 | " var p = new printStackTrace.implementation(), result = p.run(ex);\n" + 13 | " Line 4 of inline#1 script in http://path/to/file.js\n" + 14 | " printTrace(printStackTrace());\n" + 15 | " Line 7 of inline#1 script in http://path/to/file.js\n" + 16 | " bar(n - 1);\n" + 17 | " Line 11 of inline#1 script in http://path/to/file.js\n" + 18 | " bar(2);\n" + 19 | " Line 15 of inline#1 script in http://path/to/file.js\n" + 20 | " foo();\n" + 21 | "", 22 | 'opera#sourceloc': 44 23 | }; 24 | 25 | CapturedExceptions.OPERA_902 = { 26 | message: "Statement on line 44: Type mismatch (usually a non-object value used where an object is required)\n" + 27 | "Backtrace:\n" + 28 | " Line 44 of linked script http://path/to/file.js\n" + 29 | " this.undef();\n" + 30 | " Line 31 of linked script http://path/to/file.js\n" + 31 | " ex = ex || this.createException();\n" + 32 | " Line 18 of linked script http://path/to/file.js\n" + 33 | " var p = new printStackTrace.implementation(), result = p.run(ex);\n" + 34 | " Line 4 of inline#1 script in http://path/to/file.js\n" + 35 | " printTrace(printStackTrace());\n" + 36 | " Line 7 of inline#1 script in http://path/to/file.js\n" + 37 | " bar(n - 1);\n" + 38 | " Line 11 of inline#1 script in http://path/to/file.js\n" + 39 | " bar(2);\n" + 40 | " Line 15 of inline#1 script in http://path/to/file.js\n" + 41 | " foo();\n" + 42 | "", 43 | 'opera#sourceloc': 44 44 | }; 45 | 46 | CapturedExceptions.OPERA_927 = { 47 | message: "Statement on line 43: Type mismatch (usually a non-object value used where an object is required)\n" + 48 | "Backtrace:\n" + 49 | " Line 43 of linked script http://path/to/file.js\n" + 50 | " bar(n - 1);\n" + 51 | " Line 31 of linked script http://path/to/file.js\n" + 52 | " bar(2);\n" + 53 | " Line 18 of linked script http://path/to/file.js\n" + 54 | " foo();\n" + 55 | "", 56 | 'opera#sourceloc': 43 57 | }; 58 | 59 | CapturedExceptions.OPERA_964 = { 60 | message: "Statement on line 42: Type mismatch (usually non-object value supplied where object required)\n" + 61 | "Backtrace:\n" + 62 | " Line 42 of linked script http://path/to/file.js\n" + 63 | " this.undef();\n" + 64 | " Line 27 of linked script http://path/to/file.js\n" + 65 | " ex = ex || this.createException();\n" + 66 | " Line 18 of linked script http://path/to/file.js: In function printStackTrace\n" + 67 | " var p = new printStackTrace.implementation(), result = p.run(ex);\n" + 68 | " Line 4 of inline#1 script in http://path/to/file.js: In function bar\n" + 69 | " printTrace(printStackTrace());\n" + 70 | " Line 7 of inline#1 script in http://path/to/file.js: In function bar\n" + 71 | " bar(n - 1);\n" + 72 | " Line 11 of inline#1 script in http://path/to/file.js: In function foo\n" + 73 | " bar(2);\n" + 74 | " Line 15 of inline#1 script in http://path/to/file.js\n" + 75 | " foo();\n" + 76 | "", 77 | 'opera#sourceloc': 42, 78 | stacktrace: " ... Line 27 of linked script http://path/to/file.js\n" + 79 | " ex = ex || this.createException();\n" + 80 | " Line 18 of linked script http://path/to/file.js: In function printStackTrace\n" + 81 | " var p = new printStackTrace.implementation(), result = p.run(ex);\n" + 82 | " Line 4 of inline#1 script in http://path/to/file.js: In function bar\n" + 83 | " printTrace(printStackTrace());\n" + 84 | " Line 7 of inline#1 script in http://path/to/file.js: In function bar\n" + 85 | " bar(n - 1);\n" + 86 | " Line 11 of inline#1 script in http://path/to/file.js: In function foo\n" + 87 | " bar(2);\n" + 88 | " Line 15 of inline#1 script in http://path/to/file.js\n" + 89 | " foo();\n" + 90 | "" 91 | }; 92 | 93 | CapturedExceptions.OPERA_10 = { 94 | message: "Statement on line 42: Type mismatch (usually non-object value supplied where object required)", 95 | 'opera#sourceloc': 42, 96 | stacktrace: " Line 42 of linked script http://path/to/file.js\n" + 97 | " this.undef();\n" + 98 | " Line 27 of linked script http://path/to/file.js\n" + 99 | " ex = ex || this.createException();\n" + 100 | " Line 18 of linked script http://path/to/file.js: In function printStackTrace\n" + 101 | " var p = new printStackTrace.implementation(), result = p.run(ex);\n" + 102 | " Line 4 of inline#1 script in http://path/to/file.js: In function bar\n" + 103 | " printTrace(printStackTrace());\n" + 104 | " Line 7 of inline#1 script in http://path/to/file.js: In function bar\n" + 105 | " bar(n - 1);\n" + 106 | " Line 11 of inline#1 script in http://path/to/file.js: In function foo\n" + 107 | " bar(2);\n" + 108 | " Line 15 of inline#1 script in http://path/to/file.js\n" + 109 | " foo();\n" + 110 | "" 111 | }; 112 | 113 | CapturedExceptions.OPERA_11 = { 114 | message: "'this.undef' is not a function", 115 | stack: "([arguments not available])@http://path/to/file.js:27\n" + 116 | "bar([arguments not available])@http://domain.com:1234/path/to/file.js:18\n" + 117 | "foo([arguments not available])@http://domain.com:1234/path/to/file.js:11\n" + 118 | "@http://path/to/file.js:15\n" + 119 | "Error created at @http://path/to/file.js:15", 120 | stacktrace: "Error thrown at line 42, column 12 in () in http://path/to/file.js:\n" + 121 | " this.undef();\n" + 122 | "called from line 27, column 8 in (ex) in http://path/to/file.js:\n" + 123 | " ex = ex || this.createException();\n" + 124 | "called from line 18, column 4 in printStackTrace(options) in http://path/to/file.js:\n" + 125 | " var p = new printStackTrace.implementation(), result = p.run(ex);\n" + 126 | "called from line 4, column 5 in bar(n) in http://path/to/file.js:\n" + 127 | " printTrace(printStackTrace());\n" + 128 | "called from line 7, column 4 in bar(n) in http://path/to/file.js:\n" + 129 | " bar(n - 1);\n" + 130 | "called from line 11, column 4 in foo() in http://path/to/file.js:\n" + 131 | " bar(2);\n" + 132 | "called from line 15, column 3 in http://path/to/file.js:\n" + 133 | " foo();" 134 | }; 135 | 136 | CapturedExceptions.OPERA_12 = { 137 | message: "Cannot convert 'x' to object", 138 | stack: "([arguments not available])@http://localhost:8000/ExceptionLab.html:48\n" + 139 | "dumpException3([arguments not available])@http://localhost:8000/ExceptionLab.html:46\n" + 140 | "([arguments not available])@http://localhost:8000/ExceptionLab.html:1", 141 | stacktrace: "Error thrown at line 48, column 12 in (x) in http://localhost:8000/ExceptionLab.html:\n" + 142 | " x.undef();\n" + 143 | "called from line 46, column 8 in dumpException3() in http://localhost:8000/ExceptionLab.html:\n" + 144 | " dumpException((function(x) {\n" + 145 | "called from line 1, column 0 in (event) in http://localhost:8000/ExceptionLab.html:\n" + 146 | " dumpException3();" 147 | }; 148 | 149 | CapturedExceptions.OPERA_25 = { 150 | message: "Cannot read property 'undef' of null", 151 | name: "TypeError", 152 | stack: "TypeError: Cannot read property 'undef' of null\n" + 153 | " at http://path/to/file.js:47:22\n" + 154 | " at foo (http://path/to/file.js:52:15)\n" + 155 | " at bar (http://path/to/file.js:108:168)" 156 | }; 157 | 158 | CapturedExceptions.CHROME_15 = { 159 | 'arguments': ["undef"], 160 | message: "Object # has no method 'undef'", 161 | stack: "TypeError: Object # has no method 'undef'\n" + 162 | " at bar (http://path/to/file.js:13:17)\n" + 163 | " at bar (http://path/to/file.js:16:5)\n" + 164 | " at foo (http://path/to/file.js:20:5)\n" + 165 | " at http://path/to/file.js:24:4" 166 | }; 167 | 168 | CapturedExceptions.CHROME_36 = { 169 | message: "Default error", 170 | name: "Error", 171 | stack: "Error: Default error\n" + 172 | " at dumpExceptionError (http://localhost:8080/file.js:41:27)\n" + 173 | " at HTMLButtonElement.onclick (http://localhost:8080/file.js:107:146)\n" + 174 | " at I.e.fn.(anonymous function) [as index] (http://localhost:8080/file.js:10:3651)" 175 | }; 176 | 177 | // can be generated when Webpack is built with { devtool: eval } 178 | CapturedExceptions.CHROME_XX_WEBPACK = { 179 | message: "Cannot read property 'error' of undefined", 180 | name: "TypeError", 181 | stack: "TypeError: Cannot read property 'error' of undefined\n" + 182 | " at TESTTESTTEST.eval(webpack:///./src/components/test/test.jsx?:295:108)\n" + 183 | " at TESTTESTTEST.render(webpack:///./src/components/test/test.jsx?:272:32)\n" + 184 | " at TESTTESTTEST.tryRender(webpack:///./~/react-transform-catch-errors/lib/index.js?:34:31)\n" + 185 | " at TESTTESTTEST.proxiedMethod(webpack:///./~/react-proxy/modules/createPrototypeProxy.js?:44:30)" 186 | }; 187 | 188 | CapturedExceptions.FIREFOX_3 = { 189 | fileName: "http://127.0.0.1:8000/js/stacktrace.js", 190 | lineNumber: 44, 191 | message: "this.undef is not a function", 192 | name: "TypeError", 193 | stack: "()@http://127.0.0.1:8000/js/stacktrace.js:44\n" + 194 | "(null)@http://127.0.0.1:8000/js/stacktrace.js:31\n" + 195 | "printStackTrace()@http://127.0.0.1:8000/js/stacktrace.js:18\n" + 196 | "bar(1)@http://127.0.0.1:8000/js/file.js:13\n" + 197 | "bar(2)@http://127.0.0.1:8000/js/file.js:16\n" + 198 | "foo()@http://127.0.0.1:8000/js/file.js:20\n" + 199 | "@http://127.0.0.1:8000/js/file.js:24\n" + 200 | "" 201 | }; 202 | 203 | CapturedExceptions.FIREFOX_7 = { 204 | fileName: "file:///G:/js/stacktrace.js", 205 | lineNumber: 44, 206 | stack: "()@file:///G:/js/stacktrace.js:44\n" + 207 | "(null)@file:///G:/js/stacktrace.js:31\n" + 208 | "printStackTrace()@file:///G:/js/stacktrace.js:18\n" + 209 | "bar(1)@file:///G:/js/file.js:13\n" + 210 | "bar(2)@file:///G:/js/file.js:16\n" + 211 | "foo()@file:///G:/js/file.js:20\n" + 212 | "@file:///G:/js/file.js:24\n" + 213 | "" 214 | }; 215 | 216 | CapturedExceptions.FIREFOX_14 = { 217 | message: "x is null", 218 | stack: "@http://path/to/file.js:48\n" + 219 | "dumpException3@http://path/to/file.js:52\n" + 220 | "onclick@http://path/to/file.js:1\n" + 221 | "", 222 | fileName: "http://path/to/file.js", 223 | lineNumber: 48 224 | }; 225 | 226 | CapturedExceptions.FIREFOX_31 = { 227 | message: "Default error", 228 | name: "Error", 229 | stack: "foo@http://path/to/file.js:41:13\n" + 230 | "bar@http://path/to/file.js:1:1\n" + 231 | ".plugin/e.fn[c]/<@http://path/to/file.js:1:1\n" + 232 | "", 233 | fileName: "http://path/to/file.js", 234 | lineNumber: 41, 235 | columnNumber: 12 236 | }; 237 | 238 | CapturedExceptions.FIREFOX_43_EVAL = { 239 | columnNumber: 30, 240 | fileName: 'http://localhost:8080/file.js line 25 > eval line 2 > eval', 241 | lineNumber: 1, 242 | message: 'message string', 243 | stack: 'baz@http://localhost:8080/file.js line 26 > eval line 2 > eval:1:30\n' + 244 | 'foo@http://localhost:8080/file.js line 26 > eval:2:96\n' + 245 | '@http://localhost:8080/file.js line 26 > eval:4:18\n' + 246 | 'speak@http://localhost:8080/file.js:26:17\n' + 247 | '@http://localhost:8080/file.js:33:9' 248 | }; 249 | 250 | // Internal errors sometimes thrown by Firefox 251 | // More here: https://developer.mozilla.org/en-US/docs/Mozilla/Errors 252 | // 253 | // Note that such errors are instanceof "Exception", not "Error" 254 | CapturedExceptions.FIREFOX_44_NS_EXCEPTION = { 255 | message: "", 256 | name: "NS_ERROR_FAILURE", 257 | stack: "[2] tag 261 | "", 262 | fileName: "http://path/to/file.js", 263 | columnNumber: 0, 264 | lineNumber: 703, 265 | result: 2147500037 266 | }; 267 | 268 | CapturedExceptions.FIREFOX_50_RESOURCE_URL = { 269 | stack: 'render@resource://path/data/content/bundle.js:5529:16\n' + 270 | 'dispatchEvent@resource://path/data/content/vendor.bundle.js:18:23028\n' + 271 | 'wrapped@resource://path/data/content/bundle.js:7270:25', 272 | fileName: 'resource://path/data/content/bundle.js', 273 | lineNumber: 5529, 274 | columnNumber: 16, 275 | message: 'this.props.raw[this.state.dataSource].rows is undefined', 276 | name: 'TypeError' 277 | }; 278 | 279 | CapturedExceptions.SAFARI_6 = { 280 | message: "'null' is not an object (evaluating 'x.undef')", 281 | stack: "@http://path/to/file.js:48\n" + 282 | "dumpException3@http://path/to/file.js:52\n" + 283 | "onclick@http://path/to/file.js:82\n" + 284 | "[native code]", 285 | line: 48, 286 | sourceURL: "http://path/to/file.js" 287 | }; 288 | 289 | CapturedExceptions.SAFARI_7 = { 290 | message: "'null' is not an object (evaluating 'x.undef')", 291 | name: "TypeError", 292 | stack: "http://path/to/file.js:48:22\n" + 293 | "foo@http://path/to/file.js:52:15\n" + 294 | "bar@http://path/to/file.js:108:107", 295 | line: 47, 296 | sourceURL: "http://path/to/file.js" 297 | }; 298 | 299 | CapturedExceptions.SAFARI_8 = { 300 | message: "null is not an object (evaluating 'x.undef')", 301 | name: "TypeError", 302 | stack: "http://path/to/file.js:47:22\n" + 303 | "foo@http://path/to/file.js:52:15\n" + 304 | "bar@http://path/to/file.js:108:23", 305 | line: 47, 306 | column: 22, 307 | sourceURL: "http://path/to/file.js" 308 | }; 309 | 310 | CapturedExceptions.SAFARI_8_EVAL = { 311 | message: "Can't find variable: getExceptionProps", 312 | name: "ReferenceError", 313 | stack: "eval code\n" + 314 | "eval@[native code]\n" + 315 | "foo@http://path/to/file.js:58:21\n" + 316 | "bar@http://path/to/file.js:109:91", 317 | line: 1, 318 | column: 18 319 | }; 320 | 321 | CapturedExceptions.IE_9 = { 322 | message: "Unable to get property 'undef' of undefined or null reference", 323 | description: "Unable to get property 'undef' of undefined or null reference" 324 | }; 325 | 326 | CapturedExceptions.IE_10 = { 327 | message: "Unable to get property 'undef' of undefined or null reference", 328 | stack: "TypeError: Unable to get property 'undef' of undefined or null reference\n" + 329 | " at Anonymous function (http://path/to/file.js:48:13)\n" + 330 | " at foo (http://path/to/file.js:46:9)\n" + 331 | " at bar (http://path/to/file.js:82:1)", 332 | description: "Unable to get property 'undef' of undefined or null reference", 333 | number: -2146823281 334 | }; 335 | 336 | CapturedExceptions.IE_11 = { 337 | message: "Unable to get property 'undef' of undefined or null reference", 338 | name: "TypeError", 339 | stack: "TypeError: Unable to get property 'undef' of undefined or null reference\n" + 340 | " at Anonymous function (http://path/to/file.js:47:21)\n" + 341 | " at foo (http://path/to/file.js:45:13)\n" + 342 | " at bar (http://path/to/file.js:108:1)", 343 | description: "Unable to get property 'undef' of undefined or null reference", 344 | number: -2146823281 345 | }; 346 | 347 | CapturedExceptions.IE_11_EVAL = { 348 | message: "'getExceptionProps' is undefined", 349 | name: "ReferenceError", 350 | stack: "ReferenceError: 'getExceptionProps' is undefined\n" + 351 | " at eval code (eval code:1:1)\n" + 352 | " at foo (http://path/to/file.js:58:17)\n" + 353 | " at bar (http://path/to/file.js:109:1)", 354 | description: "'getExceptionProps' is undefined", 355 | number: -2146823279 356 | }; 357 | 358 | CapturedExceptions.CHROME_48_BLOB = { 359 | message: "Error: test", 360 | name: "Error", 361 | stack: "Error: test\n" + 362 | " at Error (native)\n" + 363 | " at s (blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:31:29146)\n" + 364 | " at Object.d [as add] (blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:31:30039)\n" + 365 | " at blob:http%3A//localhost%3A8080/d4eefe0f-361a-4682-b217-76587d9f712a:15:10978\n" + 366 | " at blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:1:6911\n" + 367 | " at n.fire (blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:7:3019)\n" + 368 | " at n.handle (blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:7:2863)" 369 | }; 370 | 371 | CapturedExceptions.CHROME_48_EVAL = { 372 | message: 'message string', 373 | name: 'Error', 374 | stack: 'Error: message string\n' + 375 | 'at baz (eval at foo (eval at speak (http://localhost:8080/file.js:21:17)), :1:30)\n' + 376 | 'at foo (eval at speak (http://localhost:8080/file.js:21:17), :2:96)\n' + 377 | 'at eval (eval at speak (http://localhost:8080/file.js:21:17), :4:18)\n' + 378 | 'at Object.speak (http://localhost:8080/file.js:21:17)\n' + 379 | 'at http://localhost:8080/file.js:31:13\n' 380 | }; 381 | 382 | CapturedExceptions.PHANTOMJS_1_19 = { 383 | stack: "Error: foo\n" + 384 | " at file:///path/to/file.js:878\n" + 385 | " at foo (http://path/to/file.js:4283)\n" + 386 | " at http://path/to/file.js:4287" 387 | }; 388 | 389 | CapturedExceptions.ANDROID_REACT_NATIVE = { 390 | message: 'Error: test', 391 | name: 'Error', 392 | stack: 'Error: test\n' + 393 | 'at render(/home/username/sample-workspace/sampleapp.collect.react/src/components/GpsMonitorScene.js:78:24)\n' + 394 | 'at _renderValidatedComponentWithoutOwnerOrContext(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js:1050:29)\n' + 395 | 'at _renderValidatedComponent(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js:1075:15)\n' + 396 | 'at renderedElement(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js:484:29)\n' + 397 | 'at _currentElement(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js:346:40)\n' + 398 | 'at child(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactReconciler.js:68:25)\n' + 399 | 'at children(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactMultiChild.js:264:10)\n' + 400 | 'at this(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/native/ReactNativeBaseComponent.js:74:41)\n' 401 | }; 402 | 403 | CapturedExceptions.ANDROID_REACT_NATIVE_PROD = { 404 | message: 'Error: test', 405 | name: 'Error', 406 | stack: 'value@index.android.bundle:12:1917\n' + 407 | 'onPress@index.android.bundle:12:2336\n' + 408 | 'touchableHandlePress@index.android.bundle:258:1497\n' + 409 | '[native code]\n' + 410 | '_performSideEffectsForTransition@index.android.bundle:252:8508\n' + 411 | '[native code]\n' + 412 | '_receiveSignal@index.android.bundle:252:7291\n' + 413 | '[native code]\n' + 414 | 'touchableHandleResponderRelease@index.android.bundle:252:4735\n' + 415 | '[native code]\n' + 416 | 'u@index.android.bundle:79:142\n' + 417 | 'invokeGuardedCallback@index.android.bundle:79:459\n' + 418 | 'invokeGuardedCallbackAndCatchFirstError@index.android.bundle:79:580\n' + 419 | 'c@index.android.bundle:95:365\n' + 420 | 'a@index.android.bundle:95:567\n' + 421 | 'v@index.android.bundle:146:501\n' + 422 | 'g@index.android.bundle:146:604\n' + 423 | 'forEach@[native code]\n' + 424 | 'i@index.android.bundle:149:80\n' + 425 | 'processEventQueue@index.android.bundle:146:1432\n' + 426 | 's@index.android.bundle:157:88\n' + 427 | 'handleTopLevel@index.android.bundle:157:174\n' + 428 | 'index.android.bundle:156:572\n' + 429 | 'a@index.android.bundle:93:276\n' + 430 | 'c@index.android.bundle:93:60\n' + 431 | 'perform@index.android.bundle:177:596\n' + 432 | 'batchedUpdates@index.android.bundle:188:464\n' + 433 | 'i@index.android.bundle:176:358\n' + 434 | 'i@index.android.bundle:93:90\n' + 435 | 'u@index.android.bundle:93:150\n' + 436 | '_receiveRootNodeIDEvent@index.android.bundle:156:544\n' + 437 | 'receiveTouches@index.android.bundle:156:918\n' + 438 | 'value@index.android.bundle:29:3016\n' + 439 | 'index.android.bundle:29:955\n' + 440 | 'value@index.android.bundle:29:2417\n' + 441 | 'value@index.android.bundle:29:927\n' + 442 | '[native code]' 443 | }; 444 | 445 | module.exports = CapturedExceptions; 446 | -------------------------------------------------------------------------------- /__tests__/fixtures/tracekit-resource-spec.js: -------------------------------------------------------------------------------- 1 | // 'use strict'; 2 | // 3 | // describe('Resource Error', function () { 4 | // const ErrorWatch = require('../dist/errorWatch'); 5 | // 6 | // describe('load img', function () { 7 | // let cb = null; 8 | // let img = null; 9 | // beforeEach(function () { 10 | // img = document.createElement('img'); 11 | // document.body.appendChild(img); 12 | // console.log('step1'); 13 | // }); 14 | // it('img should load error', function (done) { 15 | // function h1(stack, isWindowError, error) { 16 | // expect(stack.message).toBe(`img is load error`); 17 | // expect(stack.name).toMatch(`xxx.png`); 18 | // expect(stack.mode).toBe('resource'); 19 | // expect(stack.stack).toBe(null); 20 | // done(); 21 | // } 22 | // cb = h1; 23 | // ErrorWatch.report.subscribe(cb); 24 | // img.src = 'xxx.png'; 25 | // console.log('step2'); 26 | // }); 27 | // 28 | // afterEach(function () { 29 | // ErrorWatch.report.unsubscribe(cb); 30 | // img.remove(); 31 | // img = null; 32 | // console.log('step3'); 33 | // }); 34 | // }); 35 | // 36 | // describe('load script', function () { 37 | // let cb = null; 38 | // let script = null; 39 | // beforeEach(function () { 40 | // script = document.createElement('script'); 41 | // document.body.appendChild(script); 42 | // console.log('step4'); 43 | // }); 44 | // it('script should load error', function (done) { 45 | // function h2(stack, isWindowError, error) { 46 | // expect(stack.message).toBe(`script is load error`); 47 | // expect(stack.name).toMatch(`aaa.js`); 48 | // expect(stack.mode).toBe('resource'); 49 | // expect(stack.stack).toBe(null); 50 | // done(); 51 | // } 52 | // cb = h2; 53 | // ErrorWatch.report.subscribe(cb); 54 | // script.src = 'aaa.js'; 55 | // console.log('step5'); 56 | // }); 57 | // 58 | // afterEach(function () { 59 | // ErrorWatch.report.unsubscribe(cb); 60 | // script.remove(); 61 | // script = null; 62 | // console.log('step6'); 63 | // }); 64 | // }); 65 | // 66 | // describe('uninstallResourceError', function () { 67 | // let cb = null; 68 | // let script = null; 69 | // let timerCallback = jest.fn(); 70 | // 71 | // beforeEach(function () { 72 | // script = document.createElement('script'); 73 | // document.body.appendChild(script); 74 | // 75 | // console.log('step7'); 76 | // }); 77 | // 78 | // afterEach(function() { 79 | // // ErrorWatch.report.unsubscribe(cb); 80 | // script.remove(); 81 | // console.log('step9'); 82 | // }); 83 | // 84 | // it('not toHaveBeenCalled', function (done) { 85 | // function h3(stack, isWindowError, error) { 86 | // timerCallback(); 87 | // } 88 | // cb = h3; 89 | // ErrorWatch.report.subscribe(cb); 90 | // ErrorWatch.report.unsubscribe(cb); 91 | // script.onerror = function() { 92 | // expect(timerCallback.mock.calls.length).toBe(0); 93 | // done(); 94 | // }; 95 | // script.src = 'bbb.js'; 96 | // console.log('step8'); 97 | // }); 98 | // }); 99 | // 100 | // 101 | // }); 102 | -------------------------------------------------------------------------------- /__tests__/tracekit-computestacktrace-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('computeStackTrace', () => { 4 | describe('domain regex', () => { 5 | const regex = /(.*)\:\/\/([^\/]+)\/{0,1}([\s\S]*)/; 6 | 7 | it('should return subdomains properly', () => { 8 | const url = 'https://subdomain.yoursite.com/assets/main.js'; 9 | const domain = 'subdomain.yoursite.com'; 10 | expect(regex.exec(url)[2]).toBe(domain); 11 | }); 12 | it('should return domains correctly with any protocol', () => { 13 | const url = 'http://yoursite.com/assets/main.js'; 14 | const domain = 'yoursite.com'; 15 | 16 | expect(regex.exec(url)[2]).toBe(domain); 17 | }); 18 | it('should return the correct domain when directories match the domain', () => { 19 | const url = 'https://mysite.com/mysite/main.js'; 20 | const domain = 'mysite.com'; 21 | 22 | expect(regex.exec(url)[2]).toBe(domain); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/tracekit-handler-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Handler', function () { 4 | const ErrorWatch = require('../dist/errorWatch'); 5 | 6 | it('it should not go into an infinite loop', done => { 7 | let stacks = []; 8 | 9 | function handler(stackInfo) { 10 | stacks.push(stackInfo); 11 | } 12 | 13 | function throwException() { 14 | throw new Error('Boom!'); 15 | } 16 | 17 | ErrorWatch.report.subscribe(handler); 18 | expect(function () { 19 | ErrorWatch.wrap(throwException)(); 20 | }).toThrow(); 21 | 22 | setTimeout(function () { 23 | ErrorWatch.report.unsubscribe(handler); 24 | expect(stacks.length).toBe(1); 25 | done(); 26 | }, 1000); 27 | }, 2000); 28 | 29 | it('should get extra arguments (isWindowError and exception)', done => { 30 | const handler = jest.fn(); 31 | const exception = new Error('Boom!'); 32 | 33 | function throwException() { 34 | throw exception; 35 | } 36 | 37 | ErrorWatch.report.subscribe(handler); 38 | expect(function () { 39 | ErrorWatch.wrap(throwException)(); 40 | }).toThrow(); 41 | 42 | setTimeout(function () { 43 | ErrorWatch.report.unsubscribe(handler); 44 | 45 | expect(handler.mock.calls.length).toEqual(1); 46 | 47 | var isWindowError = handler.mock.calls[0][1]; 48 | expect(isWindowError).toEqual(false); 49 | 50 | var e = handler.mock.calls[0][2]; 51 | expect(e).toEqual(exception); 52 | 53 | done(); 54 | }, 1000); 55 | }, 2000); 56 | 57 | // NOTE: This will not pass currently because errors are rethrown. 58 | /* it('it should call report handler once', function (done){ 59 | var handlerCalledCount = 0; 60 | ErrorWatch.report.subscribe(function(stackInfo) { 61 | handlerCalledCount++; 62 | }); 63 | 64 | function handleAndReportException() { 65 | try { 66 | a++; 67 | } catch (ex) { 68 | ErrorWatch.report(ex); 69 | } 70 | } 71 | 72 | expect(handleAndReportException).not.toThrow(); 73 | setTimeout(function () { 74 | expect(handlerCalledCount).toBe(1); 75 | done(); 76 | }, 1000); 77 | }, 2000); */ 78 | }); 79 | -------------------------------------------------------------------------------- /__tests__/tracekit-parser-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Parser', function () { 4 | const ErrorWatch = require('../dist/errorWatch'); 5 | const CapturedExceptions = require('./fixtures/captured-errors'); 6 | 7 | function foo() { 8 | return bar(); 9 | } 10 | 11 | function bar() { 12 | return baz(); 13 | } 14 | 15 | function baz() { 16 | return ErrorWatch.computeStackTrace.ofCaller(); 17 | } 18 | 19 | it('should get the order of functions called right', function () { 20 | const trace = foo(); 21 | const expected = ['baz', 'bar', 'foo']; 22 | 23 | for (let i = 1; i <= 3; i++) { 24 | expect(trace.stack[i].func).toBe(expected[i - 1]); 25 | } 26 | }); 27 | 28 | it('should parse Safari 6 error', function () { 29 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.SAFARI_6); 30 | expect(stackFrames).toBeTruthy(); 31 | expect(stackFrames.stack.length).toBe(4); 32 | expect(stackFrames.stack[0]).toEqual({ url: 'http://path/to/file.js', func: '?', args: [], line: 48, column: null, context: null }); 33 | expect(stackFrames.stack[1]).toEqual({ url: 'http://path/to/file.js', func: 'dumpException3', args: [], line: 52, column: null, context: null }); 34 | expect(stackFrames.stack[2]).toEqual({ url: 'http://path/to/file.js', func: 'onclick', args: [], line: 82, column: null, context: null }); 35 | expect(stackFrames.stack[3]).toEqual({ url: '[native code]', func: '?', args: [], line: null, column: null, context: null }); 36 | }); 37 | 38 | it('should parse Safari 7 error', function () { 39 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.SAFARI_7); 40 | expect(stackFrames).toBeTruthy(); 41 | expect(stackFrames.stack.length).toBe(3); 42 | expect(stackFrames.stack[0]).toEqual({ url: 'http://path/to/file.js', func: '?', args: [], line: 48, column: 22, context: null }); 43 | expect(stackFrames.stack[1]).toEqual({ url: 'http://path/to/file.js', func: 'foo', args: [], line: 52, column: 15, context: null }); 44 | expect(stackFrames.stack[2]).toEqual({ url: 'http://path/to/file.js', func: 'bar', args: [], line: 108, column: 107, context: null }); 45 | }); 46 | 47 | it('should parse Safari 8 error', function () { 48 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.SAFARI_8); 49 | expect(stackFrames).toBeTruthy(); 50 | expect(stackFrames.stack.length).toBe(3); 51 | expect(stackFrames.stack[0]).toEqual({ url: 'http://path/to/file.js', func: '?', args: [], line: 47, column: 22, context: null }); 52 | expect(stackFrames.stack[1]).toEqual({ url: 'http://path/to/file.js', func: 'foo', args: [], line: 52, column: 15, context: null }); 53 | expect(stackFrames.stack[2]).toEqual({ url: 'http://path/to/file.js', func: 'bar', args: [], line: 108, column: 23, context: null }); 54 | }); 55 | 56 | it('should parse Safari 8 eval error', function () { 57 | // TODO: Take into account the line and column properties on the error object and use them for the first stack trace. 58 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.SAFARI_8_EVAL); 59 | expect(stackFrames).toBeTruthy(); 60 | expect(stackFrames.stack.length).toBe(3); 61 | expect(stackFrames.stack[0]).toEqual({ url: '[native code]', func: 'eval', args: [], line: null, column: null, context: null }); 62 | expect(stackFrames.stack[1]).toEqual({ url: 'http://path/to/file.js', func: 'foo', args: [], line: 58, column: 21, context: null }); 63 | expect(stackFrames.stack[2]).toEqual({ url: 'http://path/to/file.js', func: 'bar', args: [], line: 109, column: 91, context: null }); 64 | }); 65 | 66 | it('should parse Firefox 3 error', function () { 67 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.FIREFOX_3); 68 | expect(stackFrames).toBeTruthy(); 69 | expect(stackFrames.stack.length).toBe(7); 70 | expect(stackFrames.stack[0]).toEqual({ url: 'http://127.0.0.1:8000/js/stacktrace.js', func: '?', args: [], line: 44, column: null, context: null }); 71 | expect(stackFrames.stack[1]).toEqual({ url: 'http://127.0.0.1:8000/js/stacktrace.js', func: '?', args: ['null'], line: 31, column: null, context: null }); 72 | expect(stackFrames.stack[2]).toEqual({ url: 'http://127.0.0.1:8000/js/stacktrace.js', func: 'printStackTrace', args: [], line: 18, column: null, context: null }); 73 | expect(stackFrames.stack[3]).toEqual({ url: 'http://127.0.0.1:8000/js/file.js', func: 'bar', args: ['1'], line: 13, column: null, context: null }); 74 | expect(stackFrames.stack[4]).toEqual({ url: 'http://127.0.0.1:8000/js/file.js', func: 'bar', args: ['2'], line: 16, column: null, context: null }); 75 | expect(stackFrames.stack[5]).toEqual({ url: 'http://127.0.0.1:8000/js/file.js', func: 'foo', args: [], line: 20, column: null, context: null }); 76 | expect(stackFrames.stack[6]).toEqual({ url: 'http://127.0.0.1:8000/js/file.js', func: '?', args: [], line: 24, column: null, context: null }); 77 | }); 78 | 79 | it('should parse Firefox 7 error', function () { 80 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.FIREFOX_7); 81 | expect(stackFrames).toBeTruthy(); 82 | expect(stackFrames.stack.length).toBe(7); 83 | expect(stackFrames.stack[0]).toEqual({ url: 'file:///G:/js/stacktrace.js', func: '?', args: [], line: 44, column: null, context: null }); 84 | expect(stackFrames.stack[1]).toEqual({ url: 'file:///G:/js/stacktrace.js', func: '?', args: ['null'], line: 31, column: null, context: null }); 85 | expect(stackFrames.stack[2]).toEqual({ url: 'file:///G:/js/stacktrace.js', func: 'printStackTrace', args: [], line: 18, column: null, context: null }); 86 | expect(stackFrames.stack[3]).toEqual({ url: 'file:///G:/js/file.js', func: 'bar', args: ['1'], line: 13, column: null, context: null }); 87 | expect(stackFrames.stack[4]).toEqual({ url: 'file:///G:/js/file.js', func: 'bar', args: ['2'], line: 16, column: null, context: null }); 88 | expect(stackFrames.stack[5]).toEqual({ url: 'file:///G:/js/file.js', func: 'foo', args: [], line: 20, column: null, context: null }); 89 | expect(stackFrames.stack[6]).toEqual({ url: 'file:///G:/js/file.js', func: '?', args: [], line: 24, column: null, context: null }); 90 | }); 91 | 92 | it('should parse Firefox 14 error', function () { 93 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.FIREFOX_14); 94 | expect(stackFrames).toBeTruthy(); 95 | expect(stackFrames.stack.length).toBe(3); 96 | expect(stackFrames.stack[0]).toEqual({ url: 'http://path/to/file.js', func: '?', args: [], line: 48, column: null, context: null }); 97 | expect(stackFrames.stack[1]).toEqual({ url: 'http://path/to/file.js', func: 'dumpException3', args: [], line: 52, column: null, context: null }); 98 | expect(stackFrames.stack[2]).toEqual({ url: 'http://path/to/file.js', func: 'onclick', args: [], line: 1, column: null, context: null }); 99 | }); 100 | 101 | it('should parse Firefox 31 error', function () { 102 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.FIREFOX_31); 103 | expect(stackFrames).toBeTruthy(); 104 | expect(stackFrames.stack.length).toBe(3); 105 | expect(stackFrames.stack[0]).toEqual({ url: 'http://path/to/file.js', func: 'foo', args: [], line: 41, column: 13, context: null }); 106 | expect(stackFrames.stack[1]).toEqual({ url: 'http://path/to/file.js', func: 'bar', args: [], line: 1, column: 1, context: null }); 107 | expect(stackFrames.stack[2]).toEqual({ url: 'http://path/to/file.js', func: '.plugin/e.fn[c]/<', args: [], line: 1, column: 1, context: null }); 108 | }); 109 | 110 | it('should parse Firefox 44 ns exceptions', function () { 111 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.FIREFOX_44_NS_EXCEPTION); 112 | expect(stackFrames).toBeTruthy(); 113 | expect(stackFrames.stack.length).toBe(4); 114 | expect(stackFrames.stack[0]).toEqual({ url: 'http://path/to/file.js', func: '[2]', args: ['x'], line: 48, column: 12, context: [' x.undef();'] }); 293 | expect(stackFrames.stack[1]).toEqual({ url: 'http://localhost:8000/ExceptionLab.html', func: 'dumpException3', args: [], line: 46, column: 8, context: [' dumpException((function(x) {'] }); 294 | expect(stackFrames.stack[2]).toEqual({ url: 'http://localhost:8000/ExceptionLab.html', func: '', args: ['event'], line: 1, column: 0, context: [' dumpException3();'] }); 295 | }); 296 | 297 | it('should parse Opera 25 error', function () { 298 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.OPERA_25); 299 | expect(stackFrames).toBeTruthy(); 300 | expect(stackFrames.stack.length).toBe(3); 301 | expect(stackFrames.stack[0]).toEqual({ url: 'http://path/to/file.js', func: '?', args: [], line: 47, column: 22, context: null }); 302 | expect(stackFrames.stack[1]).toEqual({ url: 'http://path/to/file.js', func: 'foo', args: [], line: 52, column: 15, context: null }); 303 | expect(stackFrames.stack[2]).toEqual({ url: 'http://path/to/file.js', func: 'bar', args: [], line: 108, column: 168, context: null }); 304 | }); 305 | 306 | it('should parse PhantomJS 1.19 error', function () { 307 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.PHANTOMJS_1_19); 308 | expect(stackFrames).toBeTruthy(); 309 | expect(stackFrames.stack.length).toBe(3); 310 | expect(stackFrames.stack[0]).toEqual({ url: 'file:///path/to/file.js', func: '?', args: [], line: 878, column: null, context: null }); 311 | expect(stackFrames.stack[1]).toEqual({ url: 'http://path/to/file.js', func: 'foo', args: [], line: 4283, column: null, context: null }); 312 | expect(stackFrames.stack[2]).toEqual({ url: 'http://path/to/file.js', func: '?', args: [], line: 4287, column: null, context: null }); 313 | }); 314 | 315 | it('should parse Firefox errors with resource: URLs', function () { 316 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.FIREFOX_50_RESOURCE_URL); 317 | expect(stackFrames).toBeTruthy(); 318 | expect(stackFrames.stack.length).toBe(3); 319 | expect(stackFrames.stack[0]).toEqual({ url: 'resource://path/data/content/bundle.js', func: 'render', args: [], line: 5529, column: 16, context: null }); 320 | }); 321 | 322 | it('should parse Firefox errors with eval URLs', function () { 323 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.FIREFOX_43_EVAL); 324 | expect(stackFrames).toBeTruthy(); 325 | expect(stackFrames.stack.length).toBe(5); 326 | expect(stackFrames.stack[0]).toEqual({ url: 'http://localhost:8080/file.js', func: 'baz', args: [], line: 26, column: null, context: null }); 327 | expect(stackFrames.stack[1]).toEqual({ url: 'http://localhost:8080/file.js', func: 'foo', args: [], line: 26, column: null, context: null }); 328 | expect(stackFrames.stack[2]).toEqual({ url: 'http://localhost:8080/file.js', func: '?', args: [], line: 26, column: null, context: null }); 329 | expect(stackFrames.stack[3]).toEqual({ url: 'http://localhost:8080/file.js', func: 'speak', args: [], line: 26, column: 17, context: null }); 330 | expect(stackFrames.stack[4]).toEqual({ url: 'http://localhost:8080/file.js', func: '?', args: [], line: 33, column: 9, context: null }); 331 | }); 332 | 333 | it('should parse React Native errors on Android', function () { 334 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.ANDROID_REACT_NATIVE); 335 | expect(stackFrames).toBeTruthy(); 336 | expect(stackFrames.stack.length).toBe(8); 337 | expect(stackFrames.stack[0]).toEqual({ url: '/home/username/sample-workspace/sampleapp.collect.react/src/components/GpsMonitorScene.js', func: 'render', args: [], line: 78, column: 24, context: null }); 338 | expect(stackFrames.stack[7]).toEqual({ url: '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/native/ReactNativeBaseComponent.js', func: 'this', args: [], line: 74, column: 41, context: null }); 339 | }); 340 | 341 | it('should parse React Native errors on Android Production', function () { 342 | const stackFrames = ErrorWatch.computeStackTrace(CapturedExceptions.ANDROID_REACT_NATIVE_PROD); 343 | expect(stackFrames).toBeTruthy(); 344 | expect(stackFrames.stack.length).toBe(37); 345 | expect(stackFrames.stack[0]).toEqual({ url: 'index.android.bundle', func: 'value', args: [], line: 12, column: 1917, context: null }); 346 | expect(stackFrames.stack[35]).toEqual({ url: 'index.android.bundle', func: 'value', args: [], line: 29, column: 927, context: null }); 347 | expect(stackFrames.stack[36]).toEqual({ url: '[native code]', func: '?', args: [], line: null, column: null, context: null }); 348 | }); 349 | }); 350 | -------------------------------------------------------------------------------- /__tests__/tracekit-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ErrorWatch', function () { 4 | const ErrorWatch = require('../dist/errorWatch'); 5 | 6 | describe('General', function () { 7 | it('should not remove anonymous functions from the stack', function () { 8 | // mock up an error object with a stack trace that includes both 9 | // named functions and anonymous functions 10 | const stack_str = '' + 11 | ' Error: \n' + 12 | ' at new (http://example.com/js/test.js:63:1)\n' + // stack[0] 13 | ' at namedFunc0 (http://example.com/js/script.js:10:2)\n' + // stack[1] 14 | ' at http://example.com/js/test.js:65:10\n' + // stack[2] 15 | ' at namedFunc2 (http://example.com/js/script.js:20:5)\n' + // stack[3] 16 | ' at http://example.com/js/test.js:67:5\n' + // stack[4] 17 | ' at namedFunc4 (http://example.com/js/script.js:100001:10002)'; // stack[5] 18 | const mock_err = { stack: stack_str }; 19 | const stackFrames = ErrorWatch.computeStackTrace.computeStackTraceFromStackProp(mock_err); 20 | 21 | // Make sure ErrorWatch didn't remove the anonymous functions 22 | // from the stack like it used to :) 23 | expect(stackFrames).toBeTruthy(); 24 | expect(stackFrames.stack[0].func).toEqual('new '); 25 | expect(stackFrames.stack[0].url).toEqual('http://example.com/js/test.js'); 26 | expect(stackFrames.stack[0].line).toBe(63); 27 | expect(stackFrames.stack[0].column).toBe(1); 28 | 29 | expect(stackFrames.stack[1].func).toEqual('namedFunc0'); 30 | expect(stackFrames.stack[1].url).toEqual('http://example.com/js/script.js'); 31 | expect(stackFrames.stack[1].line).toBe(10); 32 | expect(stackFrames.stack[1].column).toBe(2); 33 | 34 | expect(stackFrames.stack[2].func).toEqual('?'); 35 | expect(stackFrames.stack[2].url).toEqual('http://example.com/js/test.js'); 36 | expect(stackFrames.stack[2].line).toBe(65); 37 | expect(stackFrames.stack[2].column).toBe(10); 38 | 39 | expect(stackFrames.stack[3].func).toEqual('namedFunc2'); 40 | expect(stackFrames.stack[3].url).toEqual('http://example.com/js/script.js'); 41 | expect(stackFrames.stack[3].line).toBe(20); 42 | expect(stackFrames.stack[3].column).toBe(5); 43 | 44 | expect(stackFrames.stack[4].func).toEqual('?'); 45 | expect(stackFrames.stack[4].url).toEqual('http://example.com/js/test.js'); 46 | expect(stackFrames.stack[4].line).toBe(67); 47 | expect(stackFrames.stack[4].column).toBe(5); 48 | 49 | expect(stackFrames.stack[5].func).toEqual('namedFunc4'); 50 | expect(stackFrames.stack[5].url).toEqual('http://example.com/js/script.js'); 51 | expect(stackFrames.stack[5].line).toBe(100001); 52 | expect(stackFrames.stack[5].column).toBe(10002); 53 | }); 54 | 55 | it('should handle eval/anonymous strings in Chrome 46', function () { 56 | const stack_str = '' + 57 | 'ReferenceError: baz is not defined\n' + 58 | ' at bar (http://example.com/js/test.js:19:7)\n' + 59 | ' at foo (http://example.com/js/test.js:23:7)\n' + 60 | ' at eval (eval at (http://example.com/js/test.js:26:5)).toBe(:1:26)\n'; 61 | 62 | const mock_err = { stack: stack_str }; 63 | const stackFrames = ErrorWatch.computeStackTrace.computeStackTraceFromStackProp(mock_err); 64 | expect(stackFrames).toBeTruthy(); 65 | expect(stackFrames.stack[0].func).toEqual('bar'); 66 | expect(stackFrames.stack[0].url).toEqual('http://example.com/js/test.js'); 67 | expect(stackFrames.stack[0].line).toBe(19); 68 | expect(stackFrames.stack[0].column).toBe(7); 69 | 70 | expect(stackFrames.stack[1].func).toEqual('foo'); 71 | expect(stackFrames.stack[1].url).toEqual('http://example.com/js/test.js'); 72 | expect(stackFrames.stack[1].line).toBe(23); 73 | expect(stackFrames.stack[1].column).toBe(7); 74 | 75 | expect(stackFrames.stack[2].func).toEqual('eval'); 76 | // TODO: fix nested evals 77 | expect(stackFrames.stack[2].url).toEqual('http://example.com/js/test.js'); 78 | expect(stackFrames.stack[2].line).toBe(26); 79 | expect(stackFrames.stack[2].column).toBe(5); 80 | }); 81 | }); 82 | 83 | describe('.computeStackTrace', function () { 84 | it('should handle a native error object', function () { 85 | const ex = new Error('test'); 86 | const stack = ErrorWatch.computeStackTrace(ex); 87 | expect(stack.name).toEqual('Error'); 88 | expect(stack.message).toEqual('test'); 89 | }); 90 | 91 | it('should handle a native error object stack from Chrome', function () { 92 | const stackStr = '' + 93 | 'Error: foo\n' + 94 | ' at :2:11\n' + 95 | ' at Object.InjectedScript._evaluateOn (:904:140)\n' + 96 | ' at Object.InjectedScript._evaluateAndWrap (:837:34)\n' + 97 | ' at Object.InjectedScript.evaluate (:693:21)'; 98 | const mockErr = { 99 | name: 'Error', 100 | message: 'foo', 101 | stack: stackStr 102 | }; 103 | const stackFrames = ErrorWatch.computeStackTrace(mockErr); 104 | expect(stackFrames).toBeTruthy(); 105 | expect(stackFrames.stack[0].url).toEqual(''); 106 | }); 107 | }); 108 | 109 | describe('error notifications', function () { 110 | const testMessage = '__mocha_ignore__'; 111 | const testLineNo = 1337; 112 | 113 | let subscriptionHandler; 114 | let oldOnErrorHandler; 115 | 116 | // ErrorWatch waits 2000ms for window.onerror to fire, so give the tests 117 | // some extra time. 118 | //this.timeout(3000); 119 | 120 | beforeEach(function () { 121 | 122 | // Prevent the onerror call that's part of our tests from getting to 123 | // mocha's handler, which would treat it as a test failure. 124 | // 125 | // We set this up here and don't ever restore the old handler, because 126 | // we can't do that without clobbering ErrorWatch's handler, which can only 127 | // be installed once. 128 | oldOnErrorHandler = window.onerror; 129 | window.onerror = function (message, url, lineNo, error) { 130 | if (message === testMessage || lineNo === testLineNo) { 131 | return true; 132 | } 133 | return oldOnErrorHandler.apply(this, arguments); 134 | }; 135 | }); 136 | 137 | afterEach(function () { 138 | window.onerror = oldOnErrorHandler; 139 | if (subscriptionHandler) { 140 | ErrorWatch.report.unsubscribe(subscriptionHandler); 141 | subscriptionHandler = null; 142 | } 143 | }); 144 | 145 | describe('with undefined arguments', function () { 146 | it('should pass undefined:undefined', function (done) { 147 | // this is probably not good behavior; just writing this test to verify 148 | // that it doesn't change unintentionally 149 | subscriptionHandler = function (stack, isWindowError, error) { 150 | expect(stack.name).toBe(undefined); 151 | expect(stack.message).toBe(undefined); 152 | done(); 153 | }; 154 | ErrorWatch.report.subscribe(subscriptionHandler); 155 | window.onerror(undefined, undefined, testLineNo); 156 | }); 157 | }); 158 | 159 | describe('when no 5th argument (error object)', function () { 160 | it('should separate name, message for default error types (e.g. ReferenceError)', function (done) { 161 | subscriptionHandler = function (stack, isWindowError, error) { 162 | expect(stack.name).toEqual('ReferenceError'); 163 | expect(stack.message).toEqual('foo is undefined'); 164 | done(); 165 | }; 166 | ErrorWatch.report.subscribe(subscriptionHandler); 167 | window.onerror('ReferenceError: foo is undefined', 'http://example.com', testLineNo); 168 | }); 169 | 170 | it('should separate name, message for default error types (e.g. Uncaught ReferenceError)', function (done) { 171 | subscriptionHandler = function (stack, isWindowError, error) { 172 | expect(stack.name).toEqual('ReferenceError'); 173 | expect(stack.message).toEqual('foo is undefined'); 174 | done(); 175 | }; 176 | ErrorWatch.report.subscribe(subscriptionHandler); 177 | // should work with/without 'Uncaught' 178 | window.onerror('Uncaught ReferenceError: foo is undefined', 'http://example.com', testLineNo); 179 | }); 180 | 181 | it('should separate name, message for default error types on Opera Mini', function (done) { 182 | subscriptionHandler = function (stack, isWindowError, error) { 183 | expect(stack.name).toEqual('ReferenceError'); 184 | expect(stack.message).toEqual('Undefined variable: foo'); 185 | done(); 186 | }; 187 | ErrorWatch.report.subscribe(subscriptionHandler); 188 | window.onerror('Uncaught exception: ReferenceError: Undefined variable: foo', 'http://example.com', testLineNo); 189 | }); 190 | 191 | it('should ignore unknown error types', function (done) { 192 | // TODO: should we attempt to parse this? 193 | subscriptionHandler = function (stack, isWindowError, error) { 194 | expect(stack.name).toBe(undefined); 195 | expect(stack.message).toEqual('CustomError: woo scary'); 196 | done(); 197 | }; 198 | ErrorWatch.report.subscribe(subscriptionHandler); 199 | window.onerror('CustomError: woo scary', 'http://example.com', testLineNo); 200 | }); 201 | 202 | it('should ignore arbitrary messages passed through onerror', function (done) { 203 | subscriptionHandler = function (stack, isWindowError, error) { 204 | expect(stack.name).toBe(undefined); 205 | expect(stack.message).toEqual('all work and no play makes homer: something something'); 206 | done(); 207 | }; 208 | ErrorWatch.report.subscribe(subscriptionHandler); 209 | window.onerror('all work and no play makes homer: something something', 'http://example.com', testLineNo); 210 | }); 211 | }); 212 | 213 | function testErrorNotification(collectWindowErrors, callOnError, numReports, done) { 214 | let numDone = 0; 215 | // ErrorWatch's collectWindowErrors flag shouldn't affect direct calls 216 | // to ErrorWatch.report, so we parameterize it for the tests. 217 | ErrorWatch.collectWindowErrors = collectWindowErrors; 218 | 219 | subscriptionHandler = function (stack, isWindowError, error) { 220 | numDone++; 221 | if (numDone == numReports) { 222 | done(); 223 | } 224 | }; 225 | ErrorWatch.report.subscribe(subscriptionHandler); 226 | 227 | // ErrorWatch.report always throws an exception in order to trigger 228 | // window.onerror so it can gather more stack data. Mocha treats 229 | // uncaught exceptions as errors, so we catch it via assert.throws 230 | // here (and manually call window.onerror later if appropriate). 231 | // 232 | // We test multiple reports because ErrorWatch has special logic for when 233 | // report() is called a second time before either a timeout elapses or 234 | // window.onerror is called (which is why we always call window.onerror 235 | // only once below, after all calls to report()). 236 | for (let i = 0; i < numReports; i++) { 237 | const e = new Error('testing'); 238 | expect(function () { 239 | ErrorWatch.report(e); 240 | }).toThrow(e); 241 | } 242 | // The call to report should work whether or not window.onerror is 243 | // triggered, so we parameterize it for the tests. We only call it 244 | // once, regardless of numReports, because the case we want to test for 245 | // multiple reports is when window.onerror is *not* called between them. 246 | if (callOnError) { 247 | window.onerror(testMessage); 248 | } 249 | } 250 | 251 | [false, true].forEach(function (collectWindowErrors) { 252 | [false, true].forEach(function (callOnError) { 253 | [1, 2].forEach(function (numReports) { 254 | it('it should receive arguments from report() when' + 255 | ' collectWindowErrors is ' + collectWindowErrors + 256 | ' and callOnError is ' + callOnError + 257 | ' and numReports is ' + numReports, function (done) { 258 | testErrorNotification(collectWindowErrors, callOnError, numReports, done); 259 | }); 260 | }); 261 | }); 262 | }); 263 | }); 264 | 265 | describe('globalHandlers', function () { 266 | let oldOnErrorHandler; 267 | let oldOnUnhandledRejectionHandler; 268 | 269 | let onErrorHandler; 270 | let onUnhandledRejectionHandler; 271 | 272 | beforeEach(function () { 273 | oldOnErrorHandler = window.onerror; 274 | oldOnUnhandledRejectionHandler = window.onunhandledrejection; 275 | 276 | onErrorHandler = function (){}; 277 | onUnhandledRejectionHandler = function (){}; 278 | 279 | window.onerror = onErrorHandler; 280 | window.onunhandledrejection = onUnhandledRejectionHandler; 281 | }); 282 | 283 | afterEach(function () { 284 | window.onerror = oldOnErrorHandler; 285 | window.onunhandledrejection = oldOnUnhandledRejectionHandler; 286 | }); 287 | 288 | describe('cleanup after unsubscribing', function () { 289 | let testHandler; 290 | 291 | beforeEach(function () { 292 | testHandler = function () { 293 | return true; 294 | }; 295 | }); 296 | 297 | it('should should restore original `window.onerror` once unsubscribed', function () { 298 | expect(window.onerror).toBe(onErrorHandler); 299 | 300 | ErrorWatch.report.subscribe(testHandler); 301 | expect(window.onerror).not.toBe(onErrorHandler); 302 | 303 | ErrorWatch.report.unsubscribe(testHandler); 304 | expect(window.onerror).toBe(onErrorHandler); 305 | }); 306 | 307 | it('should should restore original `window.onunhandledrejection` once unsubscribed', function () { 308 | expect(window.onunhandledrejection).toBe(onUnhandledRejectionHandler); 309 | 310 | ErrorWatch.report.subscribe(testHandler); 311 | expect(window.onunhandledrejection).not.toBe(onUnhandledRejectionHandler); 312 | 313 | ErrorWatch.report.unsubscribe(testHandler); 314 | expect(window.onunhandledrejection).toBe(onUnhandledRejectionHandler); 315 | }); 316 | }); 317 | }); 318 | }); 319 | -------------------------------------------------------------------------------- /dist/errorWatch.esm.js: -------------------------------------------------------------------------------- 1 | //Default options: 2 | const collectWindowErrors = true; // 是否通知 window 全局错误,开启,关掉了这个脚本就没意义了 3 | const collectSourceErrors = true; // 是否在捕获阶段获取资源加载错误,默认开启 4 | const linesOfContext = 11; // 5 lines before, the offending line, 5 lines after,没啥用 5 | const reportFuncName = 'ErrorWatch.report'; 6 | 7 | /** 8 | * A better form of hasOwnProperty
9 | * Example: `_has(MainHostObject, property) === true/false` 10 | * 11 | * @param {Object} object to check property 12 | * @param {string} key to check 13 | * @return {Boolean} true if the object has the key and it is not inherited 14 | */ 15 | function _has(object, key) { 16 | return Object.prototype.hasOwnProperty.call(object, key); 17 | } 18 | 19 | /** 20 | * Returns true if the parameter is undefined
21 | * Example: `_isUndefined(val) === true/false` 22 | * 23 | * @param {*} what Value to check 24 | * @return {Boolean} true if undefined and false otherwise 25 | */ 26 | function _isUndefined(what) { 27 | return typeof what === 'undefined'; 28 | } 29 | 30 | /** 31 | * An object representing a single stack frame. 32 | * @typedef {Object} StackFrame 33 | * @property {string} url The JavaScript or HTML file URL. 34 | * @property {string} func The function name, or empty for anonymous functions (if guessing did not work). 35 | * @property {string[]?} args The arguments passed to the function, if known. 36 | * @property {number=} line The line number, if known. 37 | * @property {number=} column The column number, if known. 38 | * @property {string[]} context An array of source code lines; the middle element corresponds to the correct line#. 39 | * @memberof ErrorWatch 40 | */ 41 | 42 | const UNKNOWN_FUNCTION = '?'; 43 | 44 | let sourceCache = {}; 45 | 46 | /** 47 | * Attempts to retrieve source code via XMLHttpRequest, which is used 48 | * to look up anonymous function names. 49 | * @param {string} url URL of source code. 50 | * @return {string} Source contents. 51 | * @memberof ErrorWatch.computeStackTrace 52 | */ 53 | function loadSource(url) { 54 | { //Only attempt request if remoteFetching is on. 55 | return ''; 56 | } 57 | } 58 | 59 | /** 60 | * Retrieves source code from the source code cache. 61 | * @param {string} url URL of source code. 62 | * @return {Array.} Source contents. 63 | * @memberof ErrorWatch.computeStackTrace 64 | */ 65 | function getSource(url) { 66 | if (typeof url !== 'string') { 67 | return []; 68 | } 69 | 70 | if (!_has(sourceCache, url)) { 71 | // URL needs to be able to fetched within the acceptable domain. Otherwise, 72 | // cross-domain errors will be triggered. 73 | /* 74 | Regex matches: 75 | 0 - Full Url 76 | 1 - Protocol 77 | 2 - Domain 78 | 3 - Port (Useful for internal applications) 79 | 4 - Path 80 | */ 81 | let source = '', 82 | domain = ''; 83 | try { domain = window.document.domain; } catch (e) { } 84 | const match = /(.*)\:\/\/([^:\/]+)([:\d]*)\/{0,1}([\s\S]*)/.exec(url); 85 | if (match && match[2] === domain) { 86 | source = loadSource(); 87 | } 88 | sourceCache[url] = source ? source.split('\n') : []; 89 | } 90 | 91 | return sourceCache[url]; 92 | } 93 | 94 | /** 95 | * Tries to use an externally loaded copy of source code to determine 96 | * the name of a function by looking at the name of the variable it was 97 | * assigned to, if any. 98 | * @param {string} url URL of source code. 99 | * @param {(string|number)} lineNo Line number in source code. 100 | * @return {string} The function name, if discoverable. 101 | * @memberof ErrorWatch.computeStackTrace 102 | */ 103 | function guessFunctionName(url, lineNo) { 104 | const reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/, 105 | reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/, 106 | maxLines = 10, 107 | source = getSource(url); 108 | let line = '', m; 109 | 110 | if (!source.length) { 111 | return UNKNOWN_FUNCTION; 112 | } 113 | 114 | // Walk backwards from the first line in the function until we find the line which 115 | // matches the pattern above, which is the function definition 116 | for (let i = 0; i < maxLines; ++i) { 117 | line = source[lineNo - i] + line; 118 | 119 | if (!_isUndefined(line)) { // 这里有个bug,永远为 true 120 | if ((m = reGuessFunction.exec(line))) { 121 | return m[1]; 122 | } else if ((m = reFunctionArgNames.exec(line))) { 123 | return m[1]; 124 | } 125 | } 126 | } 127 | 128 | return UNKNOWN_FUNCTION; 129 | } 130 | 131 | /** 132 | * Retrieves the surrounding lines from where an exception occurred. 133 | * @param {string} url URL of source code. 134 | * @param {(string|number)} line Line number in source code to center around for context. 135 | * @return {?Array.} Lines of source code. 136 | * @memberof ErrorWatch.computeStackTrace 137 | */ 138 | function gatherContext(url, line) { 139 | const source = getSource(url); 140 | 141 | if (!source.length) { 142 | return null; 143 | } 144 | 145 | let context = [], 146 | // linesBefore & linesAfter are inclusive with the offending line. 147 | // if linesOfContext is even, there will be one extra line 148 | // *before* the offending line. 149 | linesBefore = Math.floor(linesOfContext / 2), 150 | // Add one extra line if linesOfContext is odd 151 | linesAfter = linesBefore + (linesOfContext % 2), 152 | start = Math.max(0, line - linesBefore - 1), 153 | end = Math.min(source.length, line + linesAfter - 1); 154 | 155 | line -= 1; // convert to 0-based index 156 | 157 | for (let i = start; i < end; ++i) { 158 | if (!_isUndefined(source[i])) { 159 | context.push(source[i]); 160 | } 161 | } 162 | 163 | return context.length > 0 ? context : null; 164 | } 165 | 166 | /** 167 | * Escapes special characters, except for whitespace, in a string to be 168 | * used inside a regular expression as a string literal. 169 | * @param {string} text The string. 170 | * @return {string} The escaped string literal. 171 | * @memberof ErrorWatch.computeStackTrace 172 | */ 173 | function escapeRegExp(text) { 174 | return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&'); 175 | } 176 | 177 | /** 178 | * Escapes special characters in a string to be used inside a regular 179 | * expression as a string literal. Also ensures that HTML entities will 180 | * be matched the same as their literal friends. 181 | * @param {string} body The string. 182 | * @return {string} The escaped string. 183 | * @memberof ErrorWatch.computeStackTrace 184 | */ 185 | function escapeCodeAsRegExpForMatchingInsideHTML(body) { 186 | return escapeRegExp(body).replace('<', '(?:<|<)').replace('>', '(?:>|>)').replace('&', '(?:&|&)').replace('"', '(?:"|")').replace(/\s+/g, '\\s+'); 187 | } 188 | 189 | /** 190 | * Determines where a code fragment occurs in the source code. 191 | * @param {RegExp} re The function definition. 192 | * @param {Array.} urls A list of URLs to search. 193 | * @return {?Object.} An object containing 194 | * the url, line, and column number of the defined function. 195 | * @memberof ErrorWatch.computeStackTrace 196 | */ 197 | function findSourceInUrls(re, urls) { 198 | let source, m; 199 | for (let i = 0, j = urls.length; i < j; ++i) { 200 | if ((source = getSource(urls[i])).length) { 201 | source = source.join('\n'); 202 | if ((m = re.exec(source))) { 203 | 204 | return { 205 | 'url': urls[i], 206 | 'line': source.substring(0, m.index).split('\n').length, 207 | 'column': m.index - source.lastIndexOf('\n', m.index) - 1 208 | }; 209 | } 210 | } 211 | } 212 | 213 | return null; 214 | } 215 | 216 | /** 217 | * Determines at which column a code fragment occurs on a line of the 218 | * source code. 219 | * @param {string} fragment The code fragment. 220 | * @param {string} url The URL to search. 221 | * @param {(string|number)} line The line number to examine. 222 | * @return {?number} The column number. 223 | * @memberof ErrorWatch.computeStackTrace 224 | */ 225 | function findSourceInLine(fragment, url, line) { 226 | const source = getSource(url), 227 | re = new RegExp('\\b' + escapeRegExp(fragment) + '\\b'); 228 | let m; 229 | 230 | line -= 1; 231 | 232 | if (source && source.length > line && (m = re.exec(source[line]))) { 233 | return m.index; 234 | } 235 | 236 | return null; 237 | } 238 | 239 | /** 240 | * Determines where a function was defined within the source code. 241 | * @param {(Function|string)} func A function reference or serialized 242 | * function definition. 243 | * @return {?Object.} An object containing 244 | * the url, line, and column number of the defined function. 245 | * @memberof ErrorWatch.computeStackTrace 246 | */ 247 | function findSourceByFunctionBody(func) { 248 | if (_isUndefined(window && window.document)) { 249 | return null; 250 | } 251 | 252 | const urls = [window.location.href], 253 | scripts = window.document.getElementsByTagName('script'), 254 | code = '' + func, 255 | codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, 256 | eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/; 257 | let body, 258 | re, 259 | parts, 260 | result; 261 | 262 | for (let i = 0; i < scripts.length; ++i) { 263 | const script = scripts[i]; 264 | if (script.src) { 265 | urls.push(script.src); 266 | } 267 | } 268 | 269 | if (!(parts = codeRE.exec(code))) { 270 | re = new RegExp(escapeRegExp(code).replace(/\s+/g, '\\s+')); 271 | } 272 | 273 | // not sure if this is really necessary, but I don’t have a test 274 | // corpus large enough to confirm that and it was in the original. 275 | else { 276 | const name = parts[1] ? '\\s+' + parts[1] : '', 277 | args = parts[2].split(',').join('\\s*,\\s*'); 278 | 279 | body = escapeRegExp(parts[3]).replace(/;$/, ';?'); // semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+'); 280 | re = new RegExp('function' + name + '\\s*\\(\\s*' + args + '\\s*\\)\\s*{\\s*' + body + '\\s*}'); 281 | } 282 | 283 | // look for a normal function definition 284 | if ((result = findSourceInUrls(re, urls))) { 285 | return result; 286 | } 287 | 288 | // look for an old-school event handler function 289 | if ((parts = eventRE.exec(code))) { 290 | const event = parts[1]; 291 | body = escapeCodeAsRegExpForMatchingInsideHTML(parts[2]); 292 | 293 | // look for a function defined in HTML as an onXXX handler 294 | re = new RegExp('on' + event + '=[\\\'"]\\s*' + body + '\\s*[\\\'"]', 'i'); 295 | 296 | if ((result = findSourceInUrls(re, urls[0]))) { 297 | return result; 298 | } 299 | 300 | // look for ??? 301 | re = new RegExp(body); 302 | 303 | if ((result = findSourceInUrls(re, urls))) { 304 | return result; 305 | } 306 | } 307 | 308 | return null; 309 | } 310 | 311 | // Contents of Exception in various browsers. 312 | // 313 | // SAFARI: 314 | // ex.message = Can't find variable: qq 315 | // ex.line = 59 316 | // ex.sourceId = 580238192 317 | // ex.sourceURL = http://... 318 | // ex.expressionBeginOffset = 96 319 | // ex.expressionCaretOffset = 98 320 | // ex.expressionEndOffset = 98 321 | // ex.name = ReferenceError 322 | // 323 | // FIREFOX: 324 | // ex.message = qq is not defined 325 | // ex.fileName = http://... 326 | // ex.lineNumber = 59 327 | // ex.columnNumber = 69 328 | // ex.stack = ...stack trace... (see the example below) 329 | // ex.name = ReferenceError 330 | // 331 | // CHROME: 332 | // ex.message = qq is not defined 333 | // ex.name = ReferenceError 334 | // ex.type = not_defined 335 | // ex.arguments = ['aa'] 336 | // ex.stack = ...stack trace... 337 | // 338 | // INTERNET EXPLORER: 339 | // ex.message = ... 340 | // ex.name = ReferenceError 341 | // 342 | // OPERA: 343 | // ex.message = ...message... (see the example below) 344 | // ex.name = ReferenceError 345 | // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) 346 | // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' 347 | 348 | /** 349 | * Computes stack trace information from the stack property. 350 | * Chrome and Gecko use this property. 351 | * @param {Error} ex 352 | * @return {?ErrorWatch.StackTrace} Stack trace information. 353 | * @memberof ErrorWatch.computeStackTrace 354 | */ 355 | function computeStackTraceFromStackProp(ex) { 356 | if (!ex.stack) { 357 | return null; 358 | } 359 | 360 | const chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, 361 | gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i, 362 | winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, 363 | 364 | geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i, 365 | chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/, 366 | 367 | lines = ex.stack.split('\n'), 368 | reference = /^(.*) is undefined$/.exec(ex.message); 369 | let stack = [], 370 | // Used to additionally parse URL/line/column from eval frames 371 | isEval, 372 | submatch, 373 | parts, 374 | element; 375 | 376 | for (let i = 0, j = lines.length; i < j; ++i) { 377 | if ((parts = chrome.exec(lines[i]))) { 378 | const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line 379 | isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line 380 | if (isEval && (submatch = chromeEval.exec(parts[2]))) { 381 | // throw out eval line/column and use top-most line/column number 382 | parts[2] = submatch[1]; // url 383 | parts[3] = submatch[2]; // line 384 | parts[4] = submatch[3]; // column 385 | } 386 | element = { 387 | 'url': !isNative ? parts[2] : null, 388 | 'func': parts[1] || UNKNOWN_FUNCTION, 389 | 'args': isNative ? [parts[2]] : [], 390 | 'line': parts[3] ? +parts[3] : null, 391 | 'column': parts[4] ? +parts[4] : null 392 | }; 393 | } else if ( parts = winjs.exec(lines[i]) ) { 394 | element = { 395 | 'url': parts[2], 396 | 'func': parts[1] || UNKNOWN_FUNCTION, 397 | 'args': [], 398 | 'line': +parts[3], 399 | 'column': parts[4] ? +parts[4] : null 400 | }; 401 | } else if ((parts = gecko.exec(lines[i]))) { 402 | isEval = parts[3] && parts[3].indexOf(' > eval') > -1; 403 | if (isEval && (submatch = geckoEval.exec(parts[3]))) { 404 | // throw out eval line/column and use top-most line number 405 | parts[3] = submatch[1]; 406 | parts[4] = submatch[2]; 407 | parts[5] = null; // no column when eval 408 | } else if (i === 0 && !parts[5] && !_isUndefined(ex.columnNumber)) { 409 | // FireFox uses this awesome columnNumber property for its top frame 410 | // Also note, Firefox's column number is 0-based and everything else expects 1-based, 411 | // so adding 1 412 | // NOTE: this hack doesn't work if top-most frame is eval 413 | stack[0].column = ex.columnNumber + 1; 414 | } 415 | element = { 416 | 'url': parts[3], 417 | 'func': parts[1] || UNKNOWN_FUNCTION, 418 | 'args': parts[2] ? parts[2].split(',') : [], 419 | 'line': parts[4] ? +parts[4] : null, 420 | 'column': parts[5] ? +parts[5] : null 421 | }; 422 | } else { 423 | continue; 424 | } 425 | 426 | if (!element.func && element.line) { 427 | element.func = guessFunctionName(element.url, element.line); 428 | } 429 | 430 | element.context = element.line ? gatherContext(element.url, element.line) : null; 431 | stack.push(element); 432 | } 433 | 434 | if (!stack.length) { 435 | return null; 436 | } 437 | 438 | if (stack[0] && stack[0].line && !stack[0].column && reference) { 439 | stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line); 440 | } 441 | 442 | return { 443 | 'mode': 'stack', 444 | 'name': ex.name, 445 | 'message': ex.message, 446 | 'stack': stack 447 | }; 448 | } 449 | 450 | /** 451 | * Computes stack trace information from the stacktrace property. 452 | * Opera 10+ uses this property. 453 | * @param {Error} ex 454 | * @return {?ErrorWatch.StackTrace} Stack trace information. 455 | * @memberof ErrorWatch.computeStackTrace 456 | */ 457 | function computeStackTraceFromStacktraceProp(ex) { 458 | // Access and store the stacktrace property before doing ANYTHING 459 | // else to it because Opera is not very good at providing it 460 | // reliably in other circumstances. 461 | const stacktrace = ex.stacktrace; 462 | if (!stacktrace) { 463 | return; 464 | } 465 | 466 | const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i, 467 | opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i, 468 | lines = stacktrace.split('\n'); 469 | let stack = [], 470 | parts; 471 | 472 | for (let line = 0; line < lines.length; line += 2) { 473 | let element = null; 474 | if ((parts = opera10Regex.exec(lines[line]))) { 475 | element = { 476 | 'url': parts[2], 477 | 'line': +parts[1], 478 | 'column': null, 479 | 'func': parts[3], 480 | 'args':[] 481 | }; 482 | } else if ((parts = opera11Regex.exec(lines[line]))) { 483 | element = { 484 | 'url': parts[6], 485 | 'line': +parts[1], 486 | 'column': +parts[2], 487 | 'func': parts[3] || parts[4], 488 | 'args': parts[5] ? parts[5].split(',') : [] 489 | }; 490 | } 491 | 492 | if (element) { 493 | if (!element.func && element.line) { 494 | element.func = guessFunctionName(element.url, element.line); 495 | } 496 | if (element.line) { 497 | try { 498 | element.context = gatherContext(element.url, element.line); 499 | } catch (exc) {} 500 | } 501 | 502 | if (!element.context) { 503 | element.context = [lines[line + 1]]; 504 | } 505 | 506 | stack.push(element); 507 | } 508 | } 509 | 510 | if (!stack.length) { 511 | return null; 512 | } 513 | 514 | return { 515 | 'mode': 'stacktrace', 516 | 'name': ex.name, 517 | 'message': ex.message, 518 | 'stack': stack 519 | }; 520 | } 521 | 522 | /** 523 | * NOT TESTED. 524 | * Computes stack trace information from an error message that includes 525 | * the stack trace. 526 | * Opera 9 and earlier use this method if the option to show stack 527 | * traces is turned on in opera:config. 528 | * @param {Error} ex 529 | * @return {?ErrorWatch.StackTrace} Stack information. 530 | * @memberof ErrorWatch.computeStackTrace 531 | */ 532 | function computeStackTraceFromOperaMultiLineMessage(ex) { 533 | // TODO: Clean this function up 534 | // Opera includes a stack trace into the exception message. An example is: 535 | // 536 | // Statement on line 3: Undefined variable: undefinedFunc 537 | // Backtrace: 538 | // Line 3 of linked script file://localhost/Users/andreyvit/Projects/ErrorWatch/javascript-client/sample.js: In function zzz 539 | // undefinedFunc(a); 540 | // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/ErrorWatch/javascript-client/sample.html: In function yyy 541 | // zzz(x, y, z); 542 | // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/ErrorWatch/javascript-client/sample.html: In function xxx 543 | // yyy(a, a, a); 544 | // Line 1 of function script 545 | // try { xxx('hi'); return false; } catch(ex) { ErrorWatch.report(ex); } 546 | // ... 547 | 548 | const lines = ex.message.split('\n'); 549 | if (lines.length < 4) { 550 | return null; 551 | } 552 | 553 | const lineRE1 = /^\s*Line (\d+) of linked script ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i, 554 | lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i, 555 | lineRE3 = /^\s*Line (\d+) of function script\s*$/i, 556 | stack = [], 557 | scripts = (window && window.document && window.document.getElementsByTagName('script')); 558 | let inlineScriptBlocks = [], 559 | parts; 560 | 561 | for (let s in scripts) { 562 | if (_has(scripts, s) && !scripts[s].src) { 563 | inlineScriptBlocks.push(scripts[s]); 564 | } 565 | } 566 | 567 | for (let line = 2; line < lines.length; line += 2) { 568 | let item = null; 569 | if ((parts = lineRE1.exec(lines[line]))) { 570 | item = { 571 | 'url': parts[2], 572 | 'func': parts[3], 573 | 'args': [], 574 | 'line': +parts[1], 575 | 'column': null 576 | }; 577 | } else if ((parts = lineRE2.exec(lines[line]))) { 578 | item = { 579 | 'url': parts[3], 580 | 'func': parts[4], 581 | 'args': [], 582 | 'line': +parts[1], 583 | 'column': null // TODO: Check to see if inline#1 (+parts[2]) points to the script number or column number. 584 | }; 585 | const relativeLine = (+parts[1]); // relative to the start of the