├── .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 #