├── .gitignore ├── src ├── .babelrc ├── config.js ├── wrap.js ├── utils.js ├── index.js ├── tryCatch.js ├── resourceError.js ├── report.js └── computeStackTrace.js ├── __tests__ ├── tracekit-computestacktrace-spec.js ├── tracekit-handler-spec.js ├── fixtures │ ├── tracekit-resource-spec.js │ └── captured-errors.js ├── tracekit-spec.js └── tracekit-parser-spec.js ├── rollup.config.js ├── package.json ├── README.md ├── ref └── supplement.js └── dist ├── errorWatch.min.js ├── errorWatch.esm.js └── errorWatch.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | *.DS_Store 4 | *.idea 5 | *.vscode 6 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", {"modules": false}] 4 | ] 5 | } -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | //Default options: 2 | export const remoteFetching = false; // 获取远程源文件,没什么用关掉 3 | export const collectWindowErrors = true; // 是否通知 window 全局错误,开启,关掉了这个脚本就没意义了 4 | export const collectSourceErrors = true; // 是否在捕获阶段获取资源加载错误,默认开启 5 | export const linesOfContext = 11; // 5 lines before, the offending line, 5 lines after,没啥用 6 | export const debug = false; 7 | export const reportFuncName = 'ErrorWatch.report'; 8 | -------------------------------------------------------------------------------- /src/wrap.js: -------------------------------------------------------------------------------- 1 | import report from "./report"; 2 | 3 | /** 4 | * Wrap any function in a ErrorWatch reporter
5 | * Example: `func = ErrorWatch.wrap(func);` 6 | * 7 | * @param {Function} func Function to be wrapped 8 | * @return {Function} The wrapped func 9 | * @memberof ErrorWatch 10 | */ 11 | export function wrap(func) { 12 | function wrapped() { 13 | try { 14 | return func.apply(this, arguments); 15 | } catch (e) { 16 | report(e); 17 | throw e; 18 | } 19 | } 20 | return wrapped; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A better form of hasOwnProperty
3 | * Example: `_has(MainHostObject, property) === true/false` 4 | * 5 | * @param {Object} object to check property 6 | * @param {string} key to check 7 | * @return {Boolean} true if the object has the key and it is not inherited 8 | */ 9 | export function _has(object, key) { 10 | return Object.prototype.hasOwnProperty.call(object, key); 11 | } 12 | 13 | /** 14 | * Returns true if the parameter is undefined
15 | * Example: `_isUndefined(val) === true/false` 16 | * 17 | * @param {*} what Value to check 18 | * @return {Boolean} true if undefined and false otherwise 19 | */ 20 | export function _isUndefined(what) { 21 | return typeof what === 'undefined'; 22 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import report from './report'; 2 | import computeStackTrace from './computeStackTrace'; 3 | import {wrap} from './wrap'; 4 | import { extendToAsynchronousCallbacks } from './tryCatch'; 5 | 6 | const _oldErrorWatch = window.ErrorWatch; 7 | let ErrorWatch; 8 | /** 9 | * Export ErrorWatch out to another variable
10 | * Example: `var TK = ErrorWatch.noConflict()` 11 | * @return {Object} The ErrorWatch object 12 | * @memberof ErrorWatch 13 | */ 14 | function noConflict() { 15 | window.ErrorWatch = _oldErrorWatch; 16 | return ErrorWatch; 17 | } 18 | 19 | function makeError() { 20 | const tmp = thisIsAbug; 21 | return tmp + ''; 22 | } 23 | 24 | ErrorWatch = { 25 | noConflict, 26 | report, 27 | computeStackTrace, 28 | wrap, 29 | extendToAsynchronousCallbacks, 30 | makeError, 31 | }; 32 | 33 | export default ErrorWatch; 34 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // import json from 'rollup-plugin-json'; 2 | // import resolve from 'rollup-plugin-node-resolve'; 3 | import babel from 'rollup-plugin-babel'; 4 | import { uglify } from "rollup-plugin-uglify"; 5 | 6 | export default [{ 7 | input: 'src/index.js', 8 | output: { 9 | file: 'dist/errorWatch.esm.js', 10 | format: 'esm' 11 | }, 12 | }, { 13 | input: 'src/index.js', 14 | output: { 15 | // file: 'bundle.es.js', 16 | entryFileNames: 'errorWatch.js', 17 | // chunkFileNames: '[name].es[hash].js', 18 | dir: 'dist', 19 | name: 'ErrorWatch', 20 | format: 'umd' 21 | }, 22 | plugins: [ 23 | // json(), 24 | // resolve(), 25 | babel(), 26 | // uglify(), 27 | ] 28 | }, { 29 | input: 'src/index.js', 30 | output: { 31 | // file: 'bundle.es.js', 32 | entryFileNames: 'errorWatch.min.js', 33 | // chunkFileNames: '[name].es[hash].js', 34 | dir: 'dist', 35 | name: 'ErrorWatch', 36 | format: 'umd', 37 | sourcemap: true, 38 | sourcemapFile: 'errorWatch.min.js.map', 39 | }, 40 | plugins: [ 41 | // json(), 42 | // resolve(), 43 | babel(), 44 | uglify(), 45 | ] 46 | }] 47 | -------------------------------------------------------------------------------- /src/tryCatch.js: -------------------------------------------------------------------------------- 1 | import { wrap } from './wrap'; 2 | 3 | // global reference to slice 4 | const _slice = [].slice; 5 | 6 | /** 7 | * Extends support for global error handling for asynchronous browser 8 | * functions. Adopted from Closure Library's errorhandler.js 9 | * @memberof ErrorWatch 10 | */ 11 | function _helper(fnName) { 12 | const originalFn = window[fnName]; 13 | 14 | window[fnName] = function errorWatchAsyncExtension() { 15 | // Make a copy of the arguments 16 | let args = _slice.call(arguments); 17 | const originalCallback = args[0]; 18 | if (typeof (originalCallback) === 'function') { 19 | args[0] = wrap(originalCallback); 20 | } 21 | // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it 22 | // also only supports 2 argument and doesn't care what "this" is, so we 23 | // can just call the original function directly. 24 | if (originalFn.apply) { 25 | return originalFn.apply(this, args); 26 | } else { 27 | return originalFn(args[0], args[1]); 28 | } 29 | }; 30 | } 31 | 32 | export function extendToAsynchronousCallbacks() { 33 | _helper('setTimeout'); 34 | _helper('setInterval'); 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "error-watch", 3 | "version": "1.0.0", 4 | "description": "前端错误解析", 5 | "main": "dist/errorWatch.js", 6 | "module": "dist/errorWatch.esm.js", 7 | "unpkg": "dist/errorWatch.min.js", 8 | "jsdelivr": "dist/errorWatch.min.js", 9 | "scripts": { 10 | "test": "jest --color", 11 | "build": "rollup --config" 12 | }, 13 | "author": "godis", 14 | "keywords": [ 15 | "javascript", 16 | "error", 17 | "前端错误", 18 | "错误监控" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/Godiswill/errorWatch.git" 23 | }, 24 | "license": "MIT", 25 | "jest": { 26 | "testPathIgnorePatterns": [ 27 | "/node_modules/", 28 | "/__tests__/fixtures/" 29 | ] 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.10.5", 33 | "@babel/preset-env": "^7.10.4", 34 | "babel-plugin-external-helpers": "^6.22.0", 35 | "babel-preset-latest": "^6.24.1", 36 | "jest": "^26.1.0", 37 | "rollup": "^2.22.1", 38 | "rollup-plugin-babel": "^4.4.0", 39 | "rollup-plugin-json": "^4.0.0", 40 | "rollup-plugin-node-resolve": "^5.2.0", 41 | "rollup-plugin-uglify": "^6.0.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/resourceError.js: -------------------------------------------------------------------------------- 1 | import { collectSourceErrors } from './config'; 2 | 3 | let isRegisterListener = false; 4 | let _handler = null; 5 | 6 | /** 7 | * 资源加载错误上报 8 | * @param handler 9 | */ 10 | export function installResourceLoadError(handler) { 11 | if(!isRegisterListener && collectSourceErrors) { 12 | _handler = handler; 13 | window.addEventListener && window.addEventListener('error', function (e) { 14 | try { 15 | if(e.target !== window) { // 避免重复上报 16 | const stack = { 17 | message: `${e.target.localName} is load error`, 18 | mode: 'resource', 19 | name: e.target.src || e.target.href || e.target.currentSrc, 20 | stack: null, 21 | }; 22 | handler(stack, true, e); 23 | } 24 | } catch (e) { 25 | throw e; 26 | } 27 | }, true); 28 | } 29 | isRegisterListener = true; 30 | } 31 | 32 | /** 33 | * 移除资源错误加载监听 34 | */ 35 | export function uninstallResourceLoadError() { 36 | if(isRegisterListener && collectSourceErrors && _handler) { 37 | window.removeEventListener && window.removeEventListener('error', _handler); 38 | _handler = null; 39 | isRegisterListener = false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /__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__/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ref/supplement.js: -------------------------------------------------------------------------------- 1 | function exceptionalException(message) { 2 | 'use strict'; 3 | if (exceptionalException.emailErrors !== false) { 4 | exceptionalException.emailErrors = confirm('We had an error reporting an error! Please email us so we can fix it?'); 5 | } 6 | } 7 | //test 8 | //exceptionalException('try 1!'); 9 | //exceptionalException('try 2!'); 10 | 11 | //I have much better versions of the code below, you should totally bark at me if you want that code 12 | var dev = (window.localStorage ? localStorage.getItem('workingLocally') : false); 13 | 14 | /** 15 | * sendError 16 | * accepts string or object. if object, it gets stringified 17 | * if there is a failure to send due to being offline, it will retry in 2 minutes. 18 | */ 19 | function sendError(uniqueData) { 20 | 'use strict'; 21 | //hrmm.. 22 | try { 23 | if (!uniqueData.stack) { 24 | uniqueData.stack = (new Error('make stack')).stack; 25 | if (uniqueData.stack) { 26 | uniqueData.stack = uniqueData.stack.toString(); 27 | } 28 | } 29 | } catch (e) {} 30 | if (typeof uniqueData !== 'string') { 31 | uniqueData = JSON.stringify(uniqueData); 32 | } 33 | 34 | function jserrorPostFail() { 35 | checkOnline(function(online) { 36 | if (online) { 37 | //if online, alert error 38 | var args = [].slice.call(arguments, 0); 39 | var xhr; 40 | if (args[0].getAllResponseHeaders) { 41 | xhr = args[0]; 42 | } else { 43 | xhr = args[2]; 44 | } 45 | try { 46 | args.push('headers:' + xhr.getAllResponseHeaders()); 47 | } catch (e) { } 48 | args.push('uniqueData: ' + uniqueData); 49 | exceptionalException(JSON.stringify(args)); 50 | } else { 51 | //if offline, retry request 52 | console.log('failure from being offline. Will retry in 2 minutes.'); 53 | setTimeout(function offlineRetryIn2Min() { 54 | fireRequest(); 55 | }, 1000 * 60 * 2); //2 minutes 56 | } 57 | }); 58 | }; 59 | 60 | function fireRequest() { 61 | var data = {'sub.domain.com': uniqueData}; 62 | if (dev) { 63 | data = {'dev': 'test'}; //still send intentionally failiing request to better simulate production 64 | console.error(uniqueData); 65 | } 66 | console.warn('sendError'); 67 | $.ajax({ 68 | url: 'https://foo.com/jserror/', 69 | type: 'POST', //POST has no request size limit like GET 70 | data: data 71 | }) 72 | .fail(jserrorPostFail) 73 | .done(function jserrorPostDone(resp) { 74 | console.warn('sendError END ' + resp); 75 | if (resp.status === 'error') { 76 | jserrorPostFail.apply(this, arguments); 77 | } 78 | }); 79 | } 80 | fireRequest(); 81 | } 82 | 83 | TraceKit.report.subscribe(sendError); 84 | 85 | 86 | /** 87 | * Usage: 88 | * $.ajax() 89 | * .fail(ajaxFail(function(){ 90 | * //apology: alert('Sorry that action failed') 91 | * })) 92 | * 93 | * In the future, I hope to directly modify the fail function to only trigger when there's actually a server or api error 94 | * Failures due to beign offline will go into a que, and the window onoffline event will trigger. 95 | * Polling every X seconds can be done to try and get back online, and trigger window ononline handler 96 | */ 97 | function ajaxFail(apology) { 98 | if (!apology) { 99 | apology = function noop(){}; 100 | } else if (apology.getAllResponseHeaders) { 101 | alert('You are supposed to call ajaxFail like: ajaxFail(), you can pass in a callback to alert a sorry message to the user if you want.'); 102 | } 103 | return function ajaxFailFnName(xhr, status, errorThrown) { 104 | var args = [].slice.call(arguments, 0); 105 | apology.apply(this, args); 106 | var headers = xhr.getAllResponseHeaders(); 107 | if (headers) { 108 | args.push('headers:' + headers); 109 | } 110 | checkOnline(function ajaxFailCheckOffline(online) { 111 | if (online) { 112 | sendError(args); 113 | } else { 114 | //que or something to retry, but don't save to localStorage, just the in-page memory 115 | //Storing to localStorage would result in pretty unpredictable behavior 116 | //for users and probably other js code too. 117 | 118 | //I also plan to 119 | } 120 | }); 121 | }; 122 | } 123 | 124 | //checkOnline is defined in check-online.js: http://github.com/devinrhode2/check-online 125 | 126 | 127 | 128 | //OTHER I DONT KNOW POTENTIALLY BETTER VERSION 129 | 130 | var exceptionalException = function exceptionalExceptionF(message) { 131 | 'use strict'; 132 | alert('HOLY MOLY! Please email this error to support@'+location.host+'.com: \n\nSubject:Error\n' + message); 133 | }; 134 | 135 | /** 136 | * sendError 137 | * accepts string or object. if object, it gets stringified 138 | * if there is a failure to send due to being offline, it will retry in 2 minutes. 139 | */ 140 | function sendError(error) { 141 | 'use strict'; 142 | try { 143 | if (!error.stack) { 144 | error.stack = (new Error('force-added stack')).stack; 145 | if (error.stack) { 146 | error.stack = error.stack.toString(); 147 | } 148 | } 149 | } catch (e) {} 150 | 151 | if (typeof uniqueData !== 'string') { 152 | uniqueData = JSON.stringify(uniqueData); 153 | } 154 | 155 | $.ajax({ 156 | url: 'https://parsing-api.trackif.com/jserror/', 157 | type: 'POST', 158 | data: data 159 | }) 160 | .fail(jserrorPostFail) 161 | .done(function jserrorPostDone(resp) { 162 | console.warn('sendError END ' + resp); 163 | if (resp.status === 'error') { 164 | jserrorPostFail.apply(this, arguments); 165 | } 166 | }); 167 | } 168 | 169 | //override 170 | (function jQueryAjaxOverride($) { 171 | 172 | //apologizeAndReport, for when we have an error with the request and we're online: 173 | //Broken out of ajaxFail for less memory use. 174 | function apologizeAndReport(args, apology) { 175 | sendError(args); 176 | //Maybe TraceKit.report(Error('ajax error'), args); //... probably... 177 | try { 178 | //if you do $.ajax().fail(ajaxFail) this will throw because apology will the the xhr object and not a function 179 | apology && apology.apply(this, args); 180 | } catch (e) { 181 | //if you did $.ajax().fail(ajaxFail) we know what's up 182 | if (apology.getAllResponseHeaders) { 183 | alert('You are supposed to call ajaxFail like: \n' + 184 | '$.ajax().fail(ajaxFail(function(){ \n' + 185 | 'alert(\'sorry we suck\'); \n' + 186 | '}))'); 187 | } else { //else user has error in apology 188 | throw e; 189 | } 190 | } 191 | } 192 | 193 | function ajaxFail(apology) { 194 | return function ajaxFailsFnForjQuery(xhr, status, errorThrown) { 195 | checkOnline(function ajaxFailCheckOffline(online) { 196 | if (online) { 197 | //looks like we have an explicit error reponse 198 | apologizeAndReport.call(this, args, apology); 199 | } else { 200 | //que or something to retry, but don't save to localStorage, just the in-page memory 201 | //Storing to localStorage would result in pretty unpredictable behavior 202 | //for users and probably other js code too. 203 | runAjax(); 204 | } 205 | }, arguments); 206 | }; 207 | } 208 | 209 | extendFunction('$.ajax', function ajaxExtension(ajaxArgs, normalAjax) { 210 | //let's re-attempt every 2 minutes. 211 | if (ajaxArgs[1].reconnect) { 212 | 213 | } 214 | 215 | function runAjax() { 216 | var jax = normalAjax.apply(this, ajaxArgs); 217 | jax.fail = extendFunction(jax.fail, function failOverride(args, oldFail) { 218 | return oldFail( 219 | ajaxFail( 220 | args[0], //args[0] is the failureCallback 221 | ajaxArgs[1].reconnect || ajaxArgs[0].reconnect 222 | ) 223 | ); 224 | }); 225 | return jax; 226 | } 227 | return runAjax(); 228 | }); 229 | 230 | //$.ajax.reconnect(); //try reconnecting, you can manually make //return setTimeout that you can override? OR if no timeout arg, default.. 231 | 232 | }(jQuery)); 233 | -------------------------------------------------------------------------------- /dist/errorWatch.min.js: -------------------------------------------------------------------------------- 1 | !function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(n=n||self).ErrorWatch=e()}(this,function(){"use strict";var i=!0,r=!0,a=11,m="ErrorWatch.report";function b(n,e){return Object.prototype.hasOwnProperty.call(n,e)}function g(n){return void 0===n}var h="?",l={};function k(n){if("string"!=typeof n)return[];if(!b(l,n)){var e="",t="";try{t=window.document.domain}catch(n){}var r=/(.*)\:\/\/([^:\/]+)([:\d]*)\/{0,1}([\s\S]*)/.exec(n);r&&r[2]===t&&(e=""),l[n]=e?e.split("\n"):[]}return l[n]}function y(n,e){var t,r=/function ([^(]*)\(([^)]*)\)/,l=/['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/,i=k(n),u="";if(!i.length)return h;for(var c=0;c<10;++c)if(!g(u=i[e-c]+u)){if(t=l.exec(u))return t[1];if(t=r.exec(u))return t[1]}return h}function S(n,e){var t=k(n);if(!t.length)return null;var r=[],l=Math.floor(a/2),i=l+a%2,u=Math.max(0,e-l-1),c=Math.min(t.length,e+i-1);--e;for(var o=u;o","(?:>|>)").replace("&","(?:&|&)").replace('"','(?:"|")').replace(/\s+/g,"\\s+")}function $(n,e){for(var t,r,l=0,i=e.length;lt&&(r=i.exec(l[t]))?r.index:null}function u(n){if(!n.stack)return null;for(var e,t,r,l=/^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,i=/^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i,u=/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,c=/(\S+) line (\d+)(?: > eval line \d+)* > eval/i,o=/\((\S*)(?::(\d+))(?::(\d+))\)/,a=n.stack.split("\n"),s=/^(.*) is undefined$/.exec(n.message),f=[],m=0,d=a.length;m eval")&&(e=c.exec(t[3]))?(t[3]=e[1],t[4]=e[2],t[5]=null):0!==m||t[5]||g(n.columnNumber)||(f[0].column=n.columnNumber+1),r={url:t[3],func:t[1]||h,args:t[2]?t[2].split(","):[],line:t[4]?+t[4]:null,column:t[5]?+t[5]:null}}!r.func&&r.line&&(r.func=y(r.url,r.line)),r.context=r.line?S(r.url,r.line):null,f.push(r)}return f.length?(f[0]&&f[0].line&&!f[0].column&&s&&(f[0].column=w(s[1],f[0].url,f[0].line)),{mode:"stack",name:n.name,message:n.message,stack:f}):null}function p(n,e,t,r){var l={url:e,line:t};if(l.url&&l.line){n.incomplete=!1,l.func||(l.func=y(l.url,l.line)),l.context||(l.context=S(l.url,l.line));var i=/ '([^']+)' /.exec(r);if(i&&(l.column=w(i[1],l.url,l.line)),0]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i,i=e.split("\n"),u=[],c=0;c= 0; --i) { 77 | if (handlers[i] === handler) { 78 | handlers.splice(i, 1); 79 | } 80 | } 81 | 82 | if (handlers.length === 0) { 83 | uninstallGlobalHandler(); 84 | uninstallGlobalUnhandledRejectionHandler(); 85 | } 86 | } 87 | 88 | /** 89 | * Dispatch stack information to all handlers. 90 | * @param {ErrorWatch.StackTrace} stack 91 | * @param {boolean} isWindowError Is this a top-level window error? 92 | * @param {Error=} error The error that's being handled (if available, null otherwise) 93 | * @memberof ErrorWatch.report 94 | * @throws An exception if an error occurs while calling an handler. 95 | */ 96 | function notifyHandlers(stack, isWindowError, error) { 97 | let exception = null; 98 | if (isWindowError && !collectWindowErrors) { 99 | return; 100 | } 101 | for (let i in handlers) { 102 | if (_has(handlers, i)) { 103 | try { 104 | handlers[i](stack, isWindowError, error); 105 | } catch (inner) { 106 | exception = inner; 107 | } 108 | } 109 | } 110 | 111 | if (exception) { 112 | throw exception; 113 | } 114 | } 115 | 116 | let _oldOnerrorHandler, _onErrorHandlerInstalled; 117 | let _oldOnunhandledrejectionHandler, _onUnhandledRejectionHandlerInstalled; 118 | 119 | /** 120 | * Ensures all global unhandled exceptions are recorded. 121 | * Supported by Gecko and IE. 122 | * @param {string} message Error message. 123 | * @param {string} url URL of script that generated the exception. 124 | * @param {(number|string)} lineNo The line number at which the error occurred. 125 | * @param {(number|string)=} columnNo The column number at which the error occurred. 126 | * @param {Error=} errorObj The actual Error object. 127 | * @memberof ErrorWatch.report 128 | */ 129 | function errorWatchWindowOnError(message, url, lineNo, columnNo, errorObj) { 130 | let stack = null; 131 | 132 | if (lastExceptionStack) { 133 | computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message); 134 | processLastException(); 135 | } else if (errorObj) { 136 | stack = computeStackTrace(errorObj); 137 | notifyHandlers(stack, true, errorObj); 138 | } else { 139 | let location = { 140 | 'url': url, 141 | 'line': lineNo, 142 | 'column': columnNo 143 | }; 144 | 145 | let name; 146 | let msg = message; // must be new var or will modify original `arguments` 147 | if ({}.toString.call(message) === '[object String]') { 148 | const groups = message.match(ERROR_TYPES_RE); 149 | if (groups) { 150 | name = groups[1]; 151 | msg = groups[2]; 152 | } 153 | } 154 | 155 | location.func = computeStackTrace.guessFunctionName(location.url, location.line); 156 | location.context = computeStackTrace.gatherContext(location.url, location.line); 157 | stack = { 158 | 'name': name, 159 | 'message': msg, 160 | 'mode': 'onerror', 161 | 'stack': [location] 162 | }; 163 | 164 | notifyHandlers(stack, true, null); 165 | } 166 | 167 | if (_oldOnerrorHandler) { 168 | return _oldOnerrorHandler.apply(this, arguments); 169 | } 170 | 171 | return false; 172 | } 173 | 174 | /** 175 | * Ensures all unhandled rejections are recorded. 176 | * @param {PromiseRejectionEvent} e event. 177 | * @memberof ErrorWatch.report 178 | * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onunhandledrejection 179 | * @see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent 180 | */ 181 | function errorWatchWindowOnUnhandledRejection(e) { 182 | const stack = computeStackTrace(e.reason); 183 | notifyHandlers(stack, true, e.reason); 184 | } 185 | 186 | /** 187 | * Install a global onerror handler 188 | * @memberof ErrorWatch.report 189 | */ 190 | function installGlobalHandler() { 191 | if (_onErrorHandlerInstalled === true) { 192 | return; 193 | } 194 | 195 | _oldOnerrorHandler = window.onerror; 196 | window.onerror = errorWatchWindowOnError; 197 | installResourceLoadError(function handleResourceError(stack, isWindowError, error) { 198 | notifyHandlers(stack, isWindowError, error); 199 | }); 200 | _onErrorHandlerInstalled = true; 201 | } 202 | 203 | /** 204 | * Uninstall the global onerror handler 205 | * @memberof ErrorWatch.report 206 | */ 207 | function uninstallGlobalHandler() { 208 | if (_onErrorHandlerInstalled) { 209 | window.onerror = _oldOnerrorHandler; 210 | uninstallResourceLoadError(); 211 | _onErrorHandlerInstalled = false; 212 | } 213 | } 214 | 215 | /** 216 | * Install a global onunhandledrejection handler 217 | * @memberof ErrorWatch.report 218 | */ 219 | function installGlobalUnhandledRejectionHandler() { 220 | if (_onUnhandledRejectionHandlerInstalled === true) { 221 | return; 222 | } 223 | 224 | _oldOnunhandledrejectionHandler = window.onunhandledrejection; 225 | window.onunhandledrejection = errorWatchWindowOnUnhandledRejection; 226 | _onUnhandledRejectionHandlerInstalled = true; 227 | } 228 | 229 | /** 230 | * Uninstall the global onunhandledrejection handler 231 | * @memberof ErrorWatch.report 232 | */ 233 | function uninstallGlobalUnhandledRejectionHandler() { 234 | if (_onUnhandledRejectionHandlerInstalled) { 235 | window.onunhandledrejection = _oldOnunhandledrejectionHandler; 236 | _onUnhandledRejectionHandlerInstalled = false; 237 | } 238 | } 239 | 240 | /** 241 | * Process the most recent exception 242 | * @memberof ErrorWatch.report 243 | */ 244 | function processLastException() { 245 | let _lastExceptionStack = lastExceptionStack, 246 | _lastException = lastException; 247 | lastExceptionStack = null; 248 | lastException = null; 249 | notifyHandlers(_lastExceptionStack, false, _lastException); 250 | } 251 | 252 | /** 253 | * Reports an unhandled Error to ErrorWatch. 254 | * @param {Error} ex 255 | * @memberof ErrorWatch.report 256 | * @throws An exception if an incomplete stack trace is detected (old IE browsers). 257 | */ 258 | function report(ex) { 259 | if (lastExceptionStack) { 260 | if (lastException === ex) { 261 | return; // already caught by an inner catch block, ignore 262 | } else { 263 | processLastException(); 264 | } 265 | } 266 | 267 | const stack = computeStackTrace(ex); 268 | lastExceptionStack = stack; 269 | lastException = ex; 270 | 271 | // If the stack trace is incomplete, wait for 2 seconds for 272 | // slow slow IE to see if onerror occurs or not before reporting 273 | // this exception; otherwise, we will end up with an incomplete 274 | // stack trace 275 | setTimeout(function () { 276 | if (lastException === ex) { 277 | processLastException(); 278 | } 279 | }, (stack.incomplete ? 2000 : 0)); 280 | 281 | throw ex; // re-throw to propagate to the top level (and cause window.onerror) 282 | } 283 | 284 | report.subscribe = subscribe; 285 | report.unsubscribe = unsubscribe; 286 | 287 | report.__name__ = reportFuncName; 288 | 289 | export default report; 290 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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__/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 | -------------------------------------------------------------------------------- /src/computeStackTrace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An object representing a single stack frame. 3 | * @typedef {Object} StackFrame 4 | * @property {string} url The JavaScript or HTML file URL. 5 | * @property {string} func The function name, or empty for anonymous functions (if guessing did not work). 6 | * @property {string[]?} args The arguments passed to the function, if known. 7 | * @property {number=} line The line number, if known. 8 | * @property {number=} column The column number, if known. 9 | * @property {string[]} context An array of source code lines; the middle element corresponds to the correct line#. 10 | * @memberof ErrorWatch 11 | */ 12 | /** 13 | * An object representing a JavaScript stack trace. 14 | * @typedef {Object} StackTrace 15 | * @property {string} name The name of the thrown exception. 16 | * @property {string} message The exception error message. 17 | * @property {ErrorWatch.StackFrame[]} stack An array of stack frames. 18 | * @property {string} mode 'stack', 'stacktrace', 'multiline', 'callers', 'onerror', or 'failed' -- method used to collect the stack trace. 19 | * @memberof ErrorWatch 20 | */ 21 | /** 22 | * ErrorWatch.computeStackTrace: cross-browser stack traces in JavaScript 23 | * 24 | * Syntax: 25 | * ```js 26 | * s = ErrorWatch.computeStackTrace.ofCaller([depth]) 27 | * s = ErrorWatch.computeStackTrace(exception) // consider using ErrorWatch.report instead (see below) 28 | * ``` 29 | * 30 | * Supports: 31 | * - Firefox: full stack trace with line numbers and unreliable column 32 | * number on top frame 33 | * - Opera 10: full stack trace with line and column numbers 34 | * - Opera 9-: full stack trace with line numbers 35 | * - Chrome: full stack trace with line and column numbers 36 | * - Safari: line and column number for the topmost stacktrace element 37 | * only 38 | * - IE: no line numbers whatsoever 39 | * 40 | * Tries to guess names of anonymous functions by looking for assignments 41 | * in the source code. In IE and Safari, we have to guess source file names 42 | * by searching for function bodies inside all page scripts. This will not 43 | * work for scripts that are loaded cross-domain. 44 | * Here be dragons: some function names may be guessed incorrectly, and 45 | * duplicate functions may be mismatched. 46 | * 47 | * ErrorWatch.computeStackTrace should only be used for tracing purposes. 48 | * Logging of unhandled exceptions should be done with ErrorWatch.report, 49 | * which builds on top of ErrorWatch.computeStackTrace and provides better 50 | * IE support by utilizing the window.onerror event to retrieve information 51 | * about the top of the stack. 52 | * 53 | * Note: In IE and Safari, no stack trace is recorded on the Error object, 54 | * so computeStackTrace instead walks its *own* chain of callers. 55 | * This means that: 56 | * * in Safari, some methods may be missing from the stack trace; 57 | * * in IE, the topmost function in the stack trace will always be the 58 | * caller of computeStackTrace. 59 | * 60 | * This is okay for tracing (because you are likely to be calling 61 | * computeStackTrace from the function you want to be the topmost element 62 | * of the stack trace anyway), but not okay for logging unhandled 63 | * exceptions (because your catch block will likely be far away from the 64 | * inner function that actually caused the exception). 65 | * 66 | * Tracing example: 67 | * ```js 68 | * function trace(message) { 69 | * var stackInfo = ErrorWatch.computeStackTrace.ofCaller(); 70 | * var data = message + "\n"; 71 | * for(var i in stackInfo.stack) { 72 | * var item = stackInfo.stack[i]; 73 | * data += (item.func || '[anonymous]') + "() in " + item.url + ":" + (item.line || '0') + "\n"; 74 | * } 75 | * if (window.console) 76 | * console.info(data); 77 | * else 78 | * alert(data); 79 | * } 80 | * ``` 81 | * @memberof ErrorWatch 82 | * @namespace 83 | */ 84 | import { remoteFetching, linesOfContext, debug, reportFuncName } from './config'; 85 | import { _has, _isUndefined } from './utils'; 86 | 87 | const UNKNOWN_FUNCTION = '?'; 88 | 89 | let sourceCache = {}; 90 | 91 | /** 92 | * Attempts to retrieve source code via XMLHttpRequest, which is used 93 | * to look up anonymous function names. 94 | * @param {string} url URL of source code. 95 | * @return {string} Source contents. 96 | * @memberof ErrorWatch.computeStackTrace 97 | */ 98 | function loadSource(url) { 99 | if (!remoteFetching) { //Only attempt request if remoteFetching is on. 100 | return ''; 101 | } 102 | try { 103 | const getXHR = function() { 104 | try { 105 | return new window.XMLHttpRequest(); 106 | } catch (e) { 107 | // explicitly bubble up the exception if not found 108 | return new window.ActiveXObject('Microsoft.XMLHTTP'); 109 | } 110 | }; 111 | 112 | const request = getXHR(); 113 | request.open('GET', url, false); 114 | request.send(''); 115 | return request.responseText; 116 | } catch (e) { 117 | return ''; 118 | } 119 | } 120 | 121 | /** 122 | * Retrieves source code from the source code cache. 123 | * @param {string} url URL of source code. 124 | * @return {Array.} Source contents. 125 | * @memberof ErrorWatch.computeStackTrace 126 | */ 127 | function getSource(url) { 128 | if (typeof url !== 'string') { 129 | return []; 130 | } 131 | 132 | if (!_has(sourceCache, url)) { 133 | // URL needs to be able to fetched within the acceptable domain. Otherwise, 134 | // cross-domain errors will be triggered. 135 | /* 136 | Regex matches: 137 | 0 - Full Url 138 | 1 - Protocol 139 | 2 - Domain 140 | 3 - Port (Useful for internal applications) 141 | 4 - Path 142 | */ 143 | let source = '', 144 | domain = ''; 145 | try { domain = window.document.domain; } catch (e) { } 146 | const match = /(.*)\:\/\/([^:\/]+)([:\d]*)\/{0,1}([\s\S]*)/.exec(url); 147 | if (match && match[2] === domain) { 148 | source = loadSource(url); 149 | } 150 | sourceCache[url] = source ? source.split('\n') : []; 151 | } 152 | 153 | return sourceCache[url]; 154 | } 155 | 156 | /** 157 | * Tries to use an externally loaded copy of source code to determine 158 | * the name of a function by looking at the name of the variable it was 159 | * assigned to, if any. 160 | * @param {string} url URL of source code. 161 | * @param {(string|number)} lineNo Line number in source code. 162 | * @return {string} The function name, if discoverable. 163 | * @memberof ErrorWatch.computeStackTrace 164 | */ 165 | function guessFunctionName(url, lineNo) { 166 | const reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/, 167 | reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/, 168 | maxLines = 10, 169 | source = getSource(url); 170 | let line = '', m; 171 | 172 | if (!source.length) { 173 | return UNKNOWN_FUNCTION; 174 | } 175 | 176 | // Walk backwards from the first line in the function until we find the line which 177 | // matches the pattern above, which is the function definition 178 | for (let i = 0; i < maxLines; ++i) { 179 | line = source[lineNo - i] + line; 180 | 181 | if (!_isUndefined(line)) { // 这里有个bug,永远为 true 182 | if ((m = reGuessFunction.exec(line))) { 183 | return m[1]; 184 | } else if ((m = reFunctionArgNames.exec(line))) { 185 | return m[1]; 186 | } 187 | } 188 | } 189 | 190 | return UNKNOWN_FUNCTION; 191 | } 192 | 193 | /** 194 | * Retrieves the surrounding lines from where an exception occurred. 195 | * @param {string} url URL of source code. 196 | * @param {(string|number)} line Line number in source code to center around for context. 197 | * @return {?Array.} Lines of source code. 198 | * @memberof ErrorWatch.computeStackTrace 199 | */ 200 | function gatherContext(url, line) { 201 | const source = getSource(url); 202 | 203 | if (!source.length) { 204 | return null; 205 | } 206 | 207 | let context = [], 208 | // linesBefore & linesAfter are inclusive with the offending line. 209 | // if linesOfContext is even, there will be one extra line 210 | // *before* the offending line. 211 | linesBefore = Math.floor(linesOfContext / 2), 212 | // Add one extra line if linesOfContext is odd 213 | linesAfter = linesBefore + (linesOfContext % 2), 214 | start = Math.max(0, line - linesBefore - 1), 215 | end = Math.min(source.length, line + linesAfter - 1); 216 | 217 | line -= 1; // convert to 0-based index 218 | 219 | for (let i = start; i < end; ++i) { 220 | if (!_isUndefined(source[i])) { 221 | context.push(source[i]); 222 | } 223 | } 224 | 225 | return context.length > 0 ? context : null; 226 | } 227 | 228 | /** 229 | * Escapes special characters, except for whitespace, in a string to be 230 | * used inside a regular expression as a string literal. 231 | * @param {string} text The string. 232 | * @return {string} The escaped string literal. 233 | * @memberof ErrorWatch.computeStackTrace 234 | */ 235 | function escapeRegExp(text) { 236 | return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&'); 237 | } 238 | 239 | /** 240 | * Escapes special characters in a string to be used inside a regular 241 | * expression as a string literal. Also ensures that HTML entities will 242 | * be matched the same as their literal friends. 243 | * @param {string} body The string. 244 | * @return {string} The escaped string. 245 | * @memberof ErrorWatch.computeStackTrace 246 | */ 247 | function escapeCodeAsRegExpForMatchingInsideHTML(body) { 248 | return escapeRegExp(body).replace('<', '(?:<|<)').replace('>', '(?:>|>)').replace('&', '(?:&|&)').replace('"', '(?:"|")').replace(/\s+/g, '\\s+'); 249 | } 250 | 251 | /** 252 | * Determines where a code fragment occurs in the source code. 253 | * @param {RegExp} re The function definition. 254 | * @param {Array.} urls A list of URLs to search. 255 | * @return {?Object.} An object containing 256 | * the url, line, and column number of the defined function. 257 | * @memberof ErrorWatch.computeStackTrace 258 | */ 259 | function findSourceInUrls(re, urls) { 260 | let source, m; 261 | for (let i = 0, j = urls.length; i < j; ++i) { 262 | if ((source = getSource(urls[i])).length) { 263 | source = source.join('\n'); 264 | if ((m = re.exec(source))) { 265 | 266 | return { 267 | 'url': urls[i], 268 | 'line': source.substring(0, m.index).split('\n').length, 269 | 'column': m.index - source.lastIndexOf('\n', m.index) - 1 270 | }; 271 | } 272 | } 273 | } 274 | 275 | return null; 276 | } 277 | 278 | /** 279 | * Determines at which column a code fragment occurs on a line of the 280 | * source code. 281 | * @param {string} fragment The code fragment. 282 | * @param {string} url The URL to search. 283 | * @param {(string|number)} line The line number to examine. 284 | * @return {?number} The column number. 285 | * @memberof ErrorWatch.computeStackTrace 286 | */ 287 | function findSourceInLine(fragment, url, line) { 288 | const source = getSource(url), 289 | re = new RegExp('\\b' + escapeRegExp(fragment) + '\\b'); 290 | let m; 291 | 292 | line -= 1; 293 | 294 | if (source && source.length > line && (m = re.exec(source[line]))) { 295 | return m.index; 296 | } 297 | 298 | return null; 299 | } 300 | 301 | /** 302 | * Determines where a function was defined within the source code. 303 | * @param {(Function|string)} func A function reference or serialized 304 | * function definition. 305 | * @return {?Object.} An object containing 306 | * the url, line, and column number of the defined function. 307 | * @memberof ErrorWatch.computeStackTrace 308 | */ 309 | function findSourceByFunctionBody(func) { 310 | if (_isUndefined(window && window.document)) { 311 | return null; 312 | } 313 | 314 | const urls = [window.location.href], 315 | scripts = window.document.getElementsByTagName('script'), 316 | code = '' + func, 317 | codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, 318 | eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/; 319 | let body, 320 | re, 321 | parts, 322 | result; 323 | 324 | for (let i = 0; i < scripts.length; ++i) { 325 | const script = scripts[i]; 326 | if (script.src) { 327 | urls.push(script.src); 328 | } 329 | } 330 | 331 | if (!(parts = codeRE.exec(code))) { 332 | re = new RegExp(escapeRegExp(code).replace(/\s+/g, '\\s+')); 333 | } 334 | 335 | // not sure if this is really necessary, but I don’t have a test 336 | // corpus large enough to confirm that and it was in the original. 337 | else { 338 | const name = parts[1] ? '\\s+' + parts[1] : '', 339 | args = parts[2].split(',').join('\\s*,\\s*'); 340 | 341 | body = escapeRegExp(parts[3]).replace(/;$/, ';?'); // semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+'); 342 | re = new RegExp('function' + name + '\\s*\\(\\s*' + args + '\\s*\\)\\s*{\\s*' + body + '\\s*}'); 343 | } 344 | 345 | // look for a normal function definition 346 | if ((result = findSourceInUrls(re, urls))) { 347 | return result; 348 | } 349 | 350 | // look for an old-school event handler function 351 | if ((parts = eventRE.exec(code))) { 352 | const event = parts[1]; 353 | body = escapeCodeAsRegExpForMatchingInsideHTML(parts[2]); 354 | 355 | // look for a function defined in HTML as an onXXX handler 356 | re = new RegExp('on' + event + '=[\\\'"]\\s*' + body + '\\s*[\\\'"]', 'i'); 357 | 358 | if ((result = findSourceInUrls(re, urls[0]))) { 359 | return result; 360 | } 361 | 362 | // look for ??? 363 | re = new RegExp(body); 364 | 365 | if ((result = findSourceInUrls(re, urls))) { 366 | return result; 367 | } 368 | } 369 | 370 | return null; 371 | } 372 | 373 | // Contents of Exception in various browsers. 374 | // 375 | // SAFARI: 376 | // ex.message = Can't find variable: qq 377 | // ex.line = 59 378 | // ex.sourceId = 580238192 379 | // ex.sourceURL = http://... 380 | // ex.expressionBeginOffset = 96 381 | // ex.expressionCaretOffset = 98 382 | // ex.expressionEndOffset = 98 383 | // ex.name = ReferenceError 384 | // 385 | // FIREFOX: 386 | // ex.message = qq is not defined 387 | // ex.fileName = http://... 388 | // ex.lineNumber = 59 389 | // ex.columnNumber = 69 390 | // ex.stack = ...stack trace... (see the example below) 391 | // ex.name = ReferenceError 392 | // 393 | // CHROME: 394 | // ex.message = qq is not defined 395 | // ex.name = ReferenceError 396 | // ex.type = not_defined 397 | // ex.arguments = ['aa'] 398 | // ex.stack = ...stack trace... 399 | // 400 | // INTERNET EXPLORER: 401 | // ex.message = ... 402 | // ex.name = ReferenceError 403 | // 404 | // OPERA: 405 | // ex.message = ...message... (see the example below) 406 | // ex.name = ReferenceError 407 | // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) 408 | // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' 409 | 410 | /** 411 | * Computes stack trace information from the stack property. 412 | * Chrome and Gecko use this property. 413 | * @param {Error} ex 414 | * @return {?ErrorWatch.StackTrace} Stack trace information. 415 | * @memberof ErrorWatch.computeStackTrace 416 | */ 417 | function computeStackTraceFromStackProp(ex) { 418 | if (!ex.stack) { 419 | return null; 420 | } 421 | 422 | const chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, 423 | gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i, 424 | winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, 425 | 426 | geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i, 427 | chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/, 428 | 429 | lines = ex.stack.split('\n'), 430 | reference = /^(.*) is undefined$/.exec(ex.message); 431 | let stack = [], 432 | // Used to additionally parse URL/line/column from eval frames 433 | isEval, 434 | submatch, 435 | parts, 436 | element; 437 | 438 | for (let i = 0, j = lines.length; i < j; ++i) { 439 | if ((parts = chrome.exec(lines[i]))) { 440 | const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line 441 | isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line 442 | if (isEval && (submatch = chromeEval.exec(parts[2]))) { 443 | // throw out eval line/column and use top-most line/column number 444 | parts[2] = submatch[1]; // url 445 | parts[3] = submatch[2]; // line 446 | parts[4] = submatch[3]; // column 447 | } 448 | element = { 449 | 'url': !isNative ? parts[2] : null, 450 | 'func': parts[1] || UNKNOWN_FUNCTION, 451 | 'args': isNative ? [parts[2]] : [], 452 | 'line': parts[3] ? +parts[3] : null, 453 | 'column': parts[4] ? +parts[4] : null 454 | }; 455 | } else if ( parts = winjs.exec(lines[i]) ) { 456 | element = { 457 | 'url': parts[2], 458 | 'func': parts[1] || UNKNOWN_FUNCTION, 459 | 'args': [], 460 | 'line': +parts[3], 461 | 'column': parts[4] ? +parts[4] : null 462 | }; 463 | } else if ((parts = gecko.exec(lines[i]))) { 464 | isEval = parts[3] && parts[3].indexOf(' > eval') > -1; 465 | if (isEval && (submatch = geckoEval.exec(parts[3]))) { 466 | // throw out eval line/column and use top-most line number 467 | parts[3] = submatch[1]; 468 | parts[4] = submatch[2]; 469 | parts[5] = null; // no column when eval 470 | } else if (i === 0 && !parts[5] && !_isUndefined(ex.columnNumber)) { 471 | // FireFox uses this awesome columnNumber property for its top frame 472 | // Also note, Firefox's column number is 0-based and everything else expects 1-based, 473 | // so adding 1 474 | // NOTE: this hack doesn't work if top-most frame is eval 475 | stack[0].column = ex.columnNumber + 1; 476 | } 477 | element = { 478 | 'url': parts[3], 479 | 'func': parts[1] || UNKNOWN_FUNCTION, 480 | 'args': parts[2] ? parts[2].split(',') : [], 481 | 'line': parts[4] ? +parts[4] : null, 482 | 'column': parts[5] ? +parts[5] : null 483 | }; 484 | } else { 485 | continue; 486 | } 487 | 488 | if (!element.func && element.line) { 489 | element.func = guessFunctionName(element.url, element.line); 490 | } 491 | 492 | element.context = element.line ? gatherContext(element.url, element.line) : null; 493 | stack.push(element); 494 | } 495 | 496 | if (!stack.length) { 497 | return null; 498 | } 499 | 500 | if (stack[0] && stack[0].line && !stack[0].column && reference) { 501 | stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line); 502 | } 503 | 504 | return { 505 | 'mode': 'stack', 506 | 'name': ex.name, 507 | 'message': ex.message, 508 | 'stack': stack 509 | }; 510 | } 511 | 512 | /** 513 | * Computes stack trace information from the stacktrace property. 514 | * Opera 10+ uses this property. 515 | * @param {Error} ex 516 | * @return {?ErrorWatch.StackTrace} Stack trace information. 517 | * @memberof ErrorWatch.computeStackTrace 518 | */ 519 | function computeStackTraceFromStacktraceProp(ex) { 520 | // Access and store the stacktrace property before doing ANYTHING 521 | // else to it because Opera is not very good at providing it 522 | // reliably in other circumstances. 523 | const stacktrace = ex.stacktrace; 524 | if (!stacktrace) { 525 | return; 526 | } 527 | 528 | const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i, 529 | opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i, 530 | lines = stacktrace.split('\n'); 531 | let stack = [], 532 | parts; 533 | 534 | for (let line = 0; line < lines.length; line += 2) { 535 | let element = null; 536 | if ((parts = opera10Regex.exec(lines[line]))) { 537 | element = { 538 | 'url': parts[2], 539 | 'line': +parts[1], 540 | 'column': null, 541 | 'func': parts[3], 542 | 'args':[] 543 | }; 544 | } else if ((parts = opera11Regex.exec(lines[line]))) { 545 | element = { 546 | 'url': parts[6], 547 | 'line': +parts[1], 548 | 'column': +parts[2], 549 | 'func': parts[3] || parts[4], 550 | 'args': parts[5] ? parts[5].split(',') : [] 551 | }; 552 | } 553 | 554 | if (element) { 555 | if (!element.func && element.line) { 556 | element.func = guessFunctionName(element.url, element.line); 557 | } 558 | if (element.line) { 559 | try { 560 | element.context = gatherContext(element.url, element.line); 561 | } catch (exc) {} 562 | } 563 | 564 | if (!element.context) { 565 | element.context = [lines[line + 1]]; 566 | } 567 | 568 | stack.push(element); 569 | } 570 | } 571 | 572 | if (!stack.length) { 573 | return null; 574 | } 575 | 576 | return { 577 | 'mode': 'stacktrace', 578 | 'name': ex.name, 579 | 'message': ex.message, 580 | 'stack': stack 581 | }; 582 | } 583 | 584 | /** 585 | * NOT TESTED. 586 | * Computes stack trace information from an error message that includes 587 | * the stack trace. 588 | * Opera 9 and earlier use this method if the option to show stack 589 | * traces is turned on in opera:config. 590 | * @param {Error} ex 591 | * @return {?ErrorWatch.StackTrace} Stack information. 592 | * @memberof ErrorWatch.computeStackTrace 593 | */ 594 | function computeStackTraceFromOperaMultiLineMessage(ex) { 595 | // TODO: Clean this function up 596 | // Opera includes a stack trace into the exception message. An example is: 597 | // 598 | // Statement on line 3: Undefined variable: undefinedFunc 599 | // Backtrace: 600 | // Line 3 of linked script file://localhost/Users/andreyvit/Projects/ErrorWatch/javascript-client/sample.js: In function zzz 601 | // undefinedFunc(a); 602 | // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/ErrorWatch/javascript-client/sample.html: In function yyy 603 | // zzz(x, y, z); 604 | // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/ErrorWatch/javascript-client/sample.html: In function xxx 605 | // yyy(a, a, a); 606 | // Line 1 of function script 607 | // try { xxx('hi'); return false; } catch(ex) { ErrorWatch.report(ex); } 608 | // ... 609 | 610 | const lines = ex.message.split('\n'); 611 | if (lines.length < 4) { 612 | return null; 613 | } 614 | 615 | const lineRE1 = /^\s*Line (\d+) of linked script ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i, 616 | lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i, 617 | lineRE3 = /^\s*Line (\d+) of function script\s*$/i, 618 | stack = [], 619 | scripts = (window && window.document && window.document.getElementsByTagName('script')); 620 | let inlineScriptBlocks = [], 621 | parts; 622 | 623 | for (let s in scripts) { 624 | if (_has(scripts, s) && !scripts[s].src) { 625 | inlineScriptBlocks.push(scripts[s]); 626 | } 627 | } 628 | 629 | for (let line = 2; line < lines.length; line += 2) { 630 | let item = null; 631 | if ((parts = lineRE1.exec(lines[line]))) { 632 | item = { 633 | 'url': parts[2], 634 | 'func': parts[3], 635 | 'args': [], 636 | 'line': +parts[1], 637 | 'column': null 638 | }; 639 | } else if ((parts = lineRE2.exec(lines[line]))) { 640 | item = { 641 | 'url': parts[3], 642 | 'func': parts[4], 643 | 'args': [], 644 | 'line': +parts[1], 645 | 'column': null // TODO: Check to see if inline#1 (+parts[2]) points to the script number or column number. 646 | }; 647 | const relativeLine = (+parts[1]); // relative to the start of the