├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── ORIGIN.md
├── README.md
├── dist
└── jstracker.js
├── example
├── Index.vue
├── error.js
├── index.html
└── script-error
│ ├── error.js
│ └── index.html
├── package.json
├── rollup.config.js
└── src
├── monitor.js
├── try.js
└── util.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | node: true,
5 | commonjs: true,
6 | amd: true,
7 | es6: true
8 | },
9 | globals: {
10 | 'jstracker': true
11 | },
12 | extends: ['eslint:recommended'],
13 | parser: 'babel-eslint',
14 | parserOptions: {
15 | ecmaVersion: 6,
16 | sourceType: 'module',
17 | ecmaFeatures: {
18 | jsx: true,
19 | experimentalObjectRestSpread: true
20 | }
21 | },
22 | root: true,
23 | rules: {
24 | 'no-console': 0,
25 | 'indent': [2, 2],
26 | 'camelcase': [2, { properties: 'never' }],
27 | 'quotes': [2, 'single', { allowTemplateLiterals: true }],
28 | 'semi': [2, 'never'],
29 | 'eqeqeq': [2, 'always'],
30 | 'curly': [2, 'multi-line'],
31 | 'array-bracket-spacing': [2, 'never'],
32 | 'brace-style': [2, '1tbs', { allowSingleLine: true }],
33 | 'comma-dangle': [2, 'never'],
34 | 'comma-spacing': 2,
35 | 'comma-style': 2,
36 | 'computed-property-spacing': 2,
37 | 'eol-last': 2,
38 | 'func-call-spacing': 2,
39 | 'key-spacing': 2,
40 | 'keyword-spacing': 2,
41 | 'max-depth': [2, { max: 4 }],
42 | 'max-len': [2, {
43 | code: 120,
44 | ignoreUrls: true,
45 | ignoreComments: true,
46 | ignoreStrings: true,
47 | ignoreTemplateLiterals: true,
48 | ignoreRegExpLiterals: true
49 | }],
50 | 'max-lines': [2, 600],
51 | 'max-params': [2, { max: 8 }],
52 | 'max-statements-per-line': [2, { max: 2 }],
53 | 'max-statements': [1, { max: 16 }],
54 | 'new-cap': 2,
55 | 'no-array-constructor': 2,
56 | 'no-lonely-if': 2,
57 | 'no-mixed-spaces-and-tabs': 2,
58 | 'no-multiple-empty-lines': [2, { max: 2 }],
59 | 'no-multi-spaces': 2,
60 | 'no-new-object': 2,
61 | 'no-tabs': 2,
62 | 'no-trailing-spaces': 2,
63 | 'no-unneeded-ternary': 2,
64 | 'no-whitespace-before-property': 2,
65 | 'object-curly-spacing': [2, 'always'],
66 | 'one-var': [2, {
67 | var: 'never',
68 | let: 'never',
69 | const: 'never'
70 | }],
71 | 'padded-blocks': [2, 'never'],
72 | 'quote-props': [2, 'consistent'],
73 | 'valid-jsdoc': [2, {
74 | requireParamDescription: false,
75 | requireReturnDescription: false,
76 | requireReturn: false,
77 | prefer: { returns: 'return' }
78 | }],
79 | 'semi-spacing': [2, {
80 | before: false,
81 | after: true
82 | }],
83 | 'space-before-blocks': 2,
84 | 'space-before-function-paren': [2, 'never'],
85 | 'space-infix-ops': [2, { int32Hint: false }],
86 | 'space-unary-ops': 2,
87 | 'space-in-parens': 2,
88 | 'spaced-comment': [2, 'always']
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | branches:
5 | only:
6 | - master
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | > changelog
2 |
3 | ## 1.0.3 released on June 20th, 2017
4 |
5 | * 增加延时处理配置
6 |
7 | ## 1.0.2 released on June 19th, 2017
8 |
9 | 第一个公开发布版本
10 |
11 | * API 作了一些调整
12 | * 修复一些已知问题
13 |
--------------------------------------------------------------------------------
/ORIGIN.md:
--------------------------------------------------------------------------------
1 | # JS 异常的防范与监控
2 |
3 | ## 三种思路
4 |
5 | ### 主动防御
6 |
7 | 对于我们操作的数据,通常是由 API 接口返回的,时常会有一个很复杂的深层嵌套的数据结构。
8 |
9 | 为了代码的健壮性,需要对每一层访问都作空值判断,就像这样:
10 |
11 | ```js
12 | props.user &&
13 | props.user.posts &&
14 | props.user.posts[0] &&
15 | props.user.posts[0].comments &&
16 | props.user.posts[0].comments[0]
17 | ```
18 |
19 | 类似的代码大家可能都写过。看起来确实相当地不美观,有句话说得很棒:
20 |
21 | **The opposite of beautiful is not ugly, but wrong.**
22 |
23 | 参考这篇文章:[Safely Accessing Deeply Nested Values In JavaScript](https://medium.com/javascript-inside/safely-accessing-deeply-nested-values-in-javascript-99bf72a0855a)
24 |
25 | 我们可以很简单使用一种更优雅、更安全的方式访问深层嵌套数据。
26 |
27 | 使用一个简单函数
28 |
29 | ```js
30 | function getIn(p, o) {
31 | return p.reduce(function(xs, x) {
32 | return (xs && xs[x]) ? xs[x] : null;
33 | }, o);
34 | }
35 | ```
36 |
37 | 接下来我们这样访问就可以了:
38 |
39 | ```js
40 | getIn(['user', 'posts', 0, 'comments'], props)
41 | ```
42 |
43 | 如果正常访问到,则返回对应的值,否则返回 `null`。
44 |
45 | ### 全局监控
46 |
47 | * 当 JavaScript 运行时错误(包括语法错误)发生时,会执行 `window.onerror()``
48 | * 当一项资源(如 或
115 | ```
116 |
117 | 同时在 CDN 服务器增加响应头 `access-control-allow-orgin`,配置允许访问 CORS 的域
118 |
119 | #### try..catch
120 |
121 | 这一点上面也有提到,算是一种比较通用,可定制强的方案
122 |
123 | ### 错误定位
124 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jstracker: JavaScript stack trace
2 |
3 | [](https://travis-ci.org/CurtisCBS/monitor)
4 | [](https://www.npmjs.com/package/jstracker)
5 | [](https://www.npmjs.com/package/jstracker)
6 |
7 | ## 简介
8 |
9 | 通过对 error 事件的监听,获取异常相关信息并缓存,在一定时间之后报告处理。
10 |
11 | ## 功能
12 |
13 | 捕获页面 JavaScript 异常报错,捕获异常类型包含:
14 |
15 | 1. JavaScript runtime 异常捕捉 √
16 | 2. 静态资源 load faided 异常捕捉 √
17 | 3. console.error 的异常捕获 √
18 | 4. try..catch 错误捕获 √
19 | 5. 记录静态资源加载时长
20 |
21 | ## 实现概述
22 |
23 | * 通过对 [`window.onerror`](https://developer.mozilla.org/en/docs/Web/API/GlobalEventHandlers/onerror) 进行监听,捕获 JavaScript 的运行时异常,记录错误:event + 错误来源(source) + 错误行数 + 错误列数等数据
24 | * 通过对 `window.addEventListener` 监听 `error` 事件类型,获取静态资源报错,包含 JavaScript 文件,CSS 文件,图片,视频,音频。
25 | * 主要针对 vue 的异常捕获,重写了 `console.error` 事件,在捕获异常先记录错误信息的描述,再 `next` 到原始的 `console.error`
26 | * 提供包装函数对其进行 try..catch 包装,捕获异常并处理
27 |
28 | ## 使用指南
29 |
30 | ### Example
31 |
32 | #### script mode
33 |
34 | ```html
35 |
36 |
37 |
47 | ```
48 |
49 | #### module mode
50 |
51 | 1.安装
52 |
53 | ```sh
54 | npm install jstracker --save
55 | ```
56 |
57 | 2.在文件中添加
58 |
59 | ```javascript
60 | import jstracker from 'jstracker'
61 |
62 | jstracker.init({
63 | concat: false,
64 | report: function(errorLogs) {
65 | // console.log('send')
66 | }
67 | })
68 | ```
69 |
70 | ### API
71 |
72 | | 字段 | 描述 | 类型 | 默认值 | 备注 |
73 | | -------- | ----------------- | -------- | --------------------------------------- | ----------------------- |
74 | | concat | 是否延时处理,默认延时 2s 处理 | Boolean | `true` | |
75 | | delay | 错误处理间隔时间,单位 ms | Number | `2000` | 当 `concat` 为 `false` 无效 |
76 | | maxError | 一次处理的异常报错数量限制 | Number | `16` | 当 `concat` 为 `false` 无效 |
77 | | sampling | 采样率 | Number | `1` | 0 - 1 之间 |
78 | | report | 错误报告函数 | Function | `errorLogs => console.tabel(errorLogs)` | `errorLogs` 定义见下述说明 |
79 |
80 | #### 关于 errorLogs:
81 |
82 | ```javascript
83 | [
84 | {
85 | type: 1, // 参考错误类型
86 | desc: '', // 错误描述信息
87 | stack: 'no stack', // 堆栈信息。无堆栈信息时返回 'no stack'
88 | },
89 | // ...
90 | ]
91 | ```
92 |
93 | #### 错误类型
94 |
95 | ```javascript
96 | var ERROR_RUNTIME = 1
97 | var ERROR_SCRIPT = 2
98 | var ERROR_STYLE = 3
99 | var ERROR_IMAGE = 4
100 | var ERROR_AUDIO = 5
101 | var ERROR_VIDEO = 6
102 | var ERROR_CONSOLE = 7
103 | var ERROR_TRY_CATCH = 8
104 | ```
105 |
106 | ### try..catch 捕获
107 |
108 | jstracker 暴露出一个 `tryJS` 对象,可以处理 try..catch 包裹等
109 |
110 | #### 将函数使用 try..catch 包装
111 |
112 | ```javascript
113 | import jstracker from 'jstracker';
114 |
115 | this.handleSelect = jstracker.tryJS.wrap(this.handleSelect);
116 | ```
117 |
118 | #### 只包装参数
119 |
120 | ```javascript
121 | function test(type, callback) {
122 | // ...
123 | callback()
124 | }
125 |
126 | (jstracker.tryJS.wrapArgs(test))(4, function() {
127 | a = b
128 | })
129 | ```
130 |
131 | 这时候只对参数进行 try..catch 包装
132 |
133 | ## 后续功能
134 |
135 | 记录性能数据,包含:
136 |
137 | * 记录 pv 和 uv
138 | * 记录页面加载时长
139 |
140 | performance api 兼容性情况 (看到 no support 绝望,iOS不可用!)
141 |
142 | | Chrome | Edge | Firefox (Gecko) | Internet Explorer | Opera | Safari (WebKit) |
143 | | ------ | ---- | --------------- | ----------------- | ----- | --------------- |
144 | | 43.0 | yes | 41 | 10 | 33 | No support |
145 |
--------------------------------------------------------------------------------
/dist/jstracker.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (global.jstracker = factory());
5 | }(this, (function () { 'use strict';
6 |
7 | /**
8 | * debounce
9 | *
10 | * @param {Function} func 实际要执行的函数
11 | * @param {Number} delay 延迟时间,单位是 ms
12 | * @param {Function} callback 在 func 执行后的回调
13 | *
14 | * @return {Function}
15 | */
16 | function debounce(func, delay, callback) {
17 | var timer;
18 |
19 | return function() {
20 | var context = this;
21 | var args = arguments;
22 |
23 | clearTimeout(timer);
24 |
25 | timer = setTimeout(function() {
26 | func.apply(context, args);
27 |
28 | !callback || callback();
29 | }, delay);
30 | }
31 | }
32 |
33 | /**
34 | * merge
35 | *
36 | * @param {Object} src
37 | * @param {Object} dest
38 | * @return {Object}
39 | */
40 | function merge(src, dest) {
41 | for (var item in src) {
42 | dest[item] = src[item];
43 | }
44 |
45 | return dest
46 | }
47 |
48 | /**
49 | * 是否是函数
50 | *
51 | * @param {Any} func 判断对象
52 | * @return {Boolean}
53 | */
54 | function isFunction(func) {
55 | return Object.prototype.toString.call(func) === '[object Function]'
56 | }
57 |
58 | /**
59 | * 将类数组转化成数组
60 | *
61 | * @param {Object} arrayLike 类数组对象
62 | * @return {Array} 转化后的数组
63 | */
64 | function arrayFrom(arrayLike) {
65 | return [].slice.call(arrayLike)
66 | }
67 |
68 | var tryJS = {};
69 |
70 | tryJS.wrap = wrap;
71 | tryJS.wrapArgs = tryifyArgs;
72 |
73 | var config$1 = {
74 | handleTryCatchError: function() {}
75 | };
76 |
77 | function setting(opts) {
78 | merge(opts, config$1);
79 | }
80 |
81 | function wrap(func) {
82 | return isFunction(func) ? tryify(func) : func
83 | }
84 |
85 | /**
86 | * 将函数使用 try..catch 包装
87 | *
88 | * @param {Function} func 需要进行包装的函数
89 | * @return {Function} 包装后的函数
90 | */
91 | function tryify(func) {
92 | // 确保只包装一次
93 | if (!func._wrapped) {
94 | func._wrapped = function() {
95 | try {
96 | return func.apply(this, arguments)
97 | } catch (error) {
98 | config$1.handleTryCatchError(error);
99 | window.ignoreError = true;
100 |
101 | throw error
102 | }
103 | };
104 | }
105 |
106 | return func._wrapped
107 | }
108 |
109 | /**
110 | * 只对函数参数进行包装
111 | *
112 | * @param {Function} func 需要进行包装的函数
113 | * @return {Function}
114 | */
115 | function tryifyArgs(func) {
116 | return function() {
117 | var args = arrayFrom(arguments).map(function(arg) {
118 | return wrap(arg)
119 | });
120 |
121 | return func.apply(this, args)
122 | }
123 | }
124 |
125 | var monitor = {};
126 | monitor.tryJS = tryJS;
127 |
128 | setting({ handleTryCatchError: handleTryCatchError });
129 |
130 | monitor.init = function(opts) {
131 | __config(opts);
132 | __init();
133 | };
134 |
135 | // 忽略错误监听
136 | window.ignoreError = false;
137 | // 错误日志列表
138 | var errorList = [];
139 | // 错误处理回调
140 | var report = function() {};
141 |
142 | var config = {
143 | concat: true,
144 | delay: 2000, // 错误处理间隔时间
145 | maxError: 16, // 异常报错数量限制
146 | sampling: 1 // 采样率
147 | };
148 |
149 | // 定义的错误类型码
150 | var ERROR_RUNTIME = 1;
151 | var ERROR_SCRIPT = 2;
152 | var ERROR_STYLE = 3;
153 | var ERROR_IMAGE = 4;
154 | var ERROR_AUDIO = 5;
155 | var ERROR_VIDEO = 6;
156 | var ERROR_CONSOLE = 7;
157 | var ERROR_TRY_CATHC = 8;
158 |
159 | var LOAD_ERROR_TYPE = {
160 | SCRIPT: ERROR_SCRIPT,
161 | LINK: ERROR_STYLE,
162 | IMG: ERROR_IMAGE,
163 | AUDIO: ERROR_AUDIO,
164 | VIDEO: ERROR_VIDEO
165 | };
166 |
167 | function __config(opts) {
168 | merge(opts, config);
169 |
170 | report = debounce(config.report, config.delay, function() {
171 | errorList = [];
172 | });
173 | }
174 |
175 | function __init() {
176 | // 监听 JavaScript 报错异常(JavaScript runtime error)
177 | window.onerror = function() {
178 | if (window.ignoreError) {
179 | window.ignoreError = false;
180 | return
181 | }
182 |
183 | handleError(formatRuntimerError.apply(null, arguments));
184 | };
185 |
186 | // 监听资源加载错误(JavaScript Scource failed to load)
187 | window.addEventListener('error', function(event) {
188 | // 过滤 target 为 window 的异常,避免与上面的 onerror 重复
189 | var errorTarget = event.target;
190 | if (errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]) {
191 | handleError(formatLoadError(errorTarget));
192 | }
193 | }, true);
194 |
195 | // 针对 vue 报错重写 console.error
196 | // TODO
197 | console.error = (function(origin) {
198 | return function(info) {
199 | var errorLog = {
200 | type: ERROR_CONSOLE,
201 | desc: info
202 | };
203 |
204 | handleError(errorLog);
205 | origin.call(console, info);
206 | }
207 | })(console.error);
208 | }
209 |
210 | // 处理 try..catch 错误
211 | function handleTryCatchError(error) {
212 | handleError(formatTryCatchError(error));
213 | }
214 |
215 | /**
216 | * 生成 runtime 错误日志
217 | *
218 | * @param {String} message 错误信息
219 | * @param {String} source 发生错误的脚本 URL
220 | * @param {Number} lineno 发生错误的行号
221 | * @param {Number} colno 发生错误的列号
222 | * @param {Object} error error 对象
223 | * @return {Object}
224 | */
225 | function formatRuntimerError(message, source, lineno, colno, error) {
226 | return {
227 | type: ERROR_RUNTIME,
228 | desc: message + ' at ' + source + ':' + lineno + ':' + colno,
229 | stack: error && error.stack ? error.stack : 'no stack' // IE <9, has no error stack
230 | }
231 | }
232 |
233 | /**
234 | * 生成 laod 错误日志
235 | *
236 | * @param {Object} errorTarget
237 | * @return {Object}
238 | */
239 | function formatLoadError(errorTarget) {
240 | return {
241 | type: LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()],
242 | desc: errorTarget.baseURI + '@' + (errorTarget.src || errorTarget.href),
243 | stack: 'no stack'
244 | }
245 | }
246 |
247 | /**
248 | * 生成 try..catch 错误日志
249 | *
250 | * @param {Object} error error 对象
251 | * @return {Object} 格式化后的对象
252 | */
253 | function formatTryCatchError(error) {
254 | return {
255 | type: ERROR_TRY_CATHC,
256 | desc: error.message,
257 | stack: error.stack
258 | }
259 | }
260 |
261 | /**
262 | * 错误数据预处理
263 | *
264 | * @param {Object} errorLog 错误日志
265 | */
266 | function handleError(errorLog) {
267 | // 是否延时处理
268 | if (!config.concat) {
269 | !needReport(config.sampling) || config.report([errorLog]);
270 | } else {
271 | pushError(errorLog);
272 | report(errorList);
273 | }
274 | }
275 |
276 | /**
277 | * 往异常信息数组里面添加一条记录
278 | *
279 | * @param {Object} errorLog 错误日志
280 | */
281 | function pushError(errorLog) {
282 | if (needReport(config.sampling) && errorList.length < config.maxError) {
283 | errorList.push(errorLog);
284 | }
285 | }
286 |
287 | /**
288 | * 设置一个采样率,决定是否上报
289 | *
290 | * @param {Number} sampling 0 - 1
291 | * @return {Boolean}
292 | */
293 | function needReport(sampling) {
294 | return Math.random() < (sampling || 1)
295 | }
296 |
297 | return monitor;
298 |
299 | })));
300 |
--------------------------------------------------------------------------------
/example/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | x轴维度:
5 |
8 |
9 |