├── .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 | [![build status](https://img.shields.io/travis/CurtisCBS/monitor/master.svg?style=flat-square)](https://travis-ci.org/CurtisCBS/monitor) 4 | [![npm version](https://img.shields.io/npm/v/jstracker.svg?style=flat-square)](https://www.npmjs.com/package/jstracker) 5 | [![npm downloads](https://img.shields.io/npm/dm/jstracker.svg?style=flat-square)](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 | 26 | 127 | 147 | -------------------------------------------------------------------------------- /example/error.js: -------------------------------------------------------------------------------- 1 | var u = o 2 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example for monitor 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 30 | 31 | 32 | 35 | 36 | 39 | 40 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /example/script-error/error.js: -------------------------------------------------------------------------------- 1 | function clickHandle() { 2 | var b = a.c 3 | } 4 | 5 | console.log(jstracker) 6 | 7 | jstracker.init({ 8 | report: function(errorLogs) { 9 | console.table(errorLogs) 10 | console.log('发送请求') 11 | } 12 | }) 13 | 14 | var clickHandleTry = jstracker.tryJS.wrap(clickHandle) 15 | document.querySelector('.send').addEventListener('click', clickHandleTry) 16 | 17 | 18 | function goHome(type, callback) { 19 | console.log(type) 20 | 21 | callback() 22 | } 23 | 24 | // goHome = tryJS.wrap(goHome) 25 | // goHome(4, function() { 26 | // console.log('done') 27 | // console.log(ming = tian) 28 | // }) 29 | 30 | (jstracker.tryJS.wrapArgs(goHome))(4, function() { 31 | console.log('done') 32 | ming = tian 33 | }) 34 | -------------------------------------------------------------------------------- /example/script-error/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tracker 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstracker", 3 | "version": "1.0.3", 4 | "description": "JavaScript stack trace", 5 | "main": "dist/jstracker.js", 6 | "scripts": { 7 | "build": "rollup -c -w" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/CurtisCBS/monitor.git" 12 | }, 13 | "keywords": [ 14 | "monitor", 15 | "jstracker" 16 | ], 17 | "author": "", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/CurtisCBS/monitor/issues" 21 | }, 22 | "homepage": "https://github.com/CurtisCBS/monitor#readme", 23 | "devDependencies": { 24 | "babel-eslint": "^7.2.3", 25 | "eslint": "^4.0.0", 26 | "rollup": "^0.43.0", 27 | "rollup-watch": "^4.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import config from './package.json' 2 | 3 | const name = config.name 4 | 5 | export default { 6 | entry: 'src/monitor.js', 7 | format: 'umd', 8 | moduleName: name, 9 | dest: `dist/${name}.js` 10 | } 11 | -------------------------------------------------------------------------------- /src/monitor.js: -------------------------------------------------------------------------------- 1 | import tryJS, { setting } from './try' 2 | import { 3 | debounce, 4 | merge 5 | } from './util' 6 | 7 | var monitor = {} 8 | monitor.tryJS = tryJS 9 | 10 | setting({ handleTryCatchError: handleTryCatchError }) 11 | 12 | monitor.init = function(opts) { 13 | __config(opts) 14 | __init() 15 | } 16 | 17 | // 忽略错误监听 18 | window.ignoreError = false 19 | // 错误日志列表 20 | var errorList = [] 21 | // 错误处理回调 22 | var report = function() {} 23 | 24 | var config = { 25 | concat: true, 26 | delay: 2000, // 错误处理间隔时间 27 | maxError: 16, // 异常报错数量限制 28 | sampling: 1 // 采样率 29 | } 30 | 31 | // 定义的错误类型码 32 | var ERROR_RUNTIME = 1 33 | var ERROR_SCRIPT = 2 34 | var ERROR_STYLE = 3 35 | var ERROR_IMAGE = 4 36 | var ERROR_AUDIO = 5 37 | var ERROR_VIDEO = 6 38 | var ERROR_CONSOLE = 7 39 | var ERROR_TRY_CATHC = 8 40 | 41 | var LOAD_ERROR_TYPE = { 42 | SCRIPT: ERROR_SCRIPT, 43 | LINK: ERROR_STYLE, 44 | IMG: ERROR_IMAGE, 45 | AUDIO: ERROR_AUDIO, 46 | VIDEO: ERROR_VIDEO 47 | } 48 | 49 | function __config(opts) { 50 | merge(opts, config) 51 | 52 | report = debounce(config.report, config.delay, function() { 53 | errorList = [] 54 | }) 55 | } 56 | 57 | function __init() { 58 | // 监听 JavaScript 报错异常(JavaScript runtime error) 59 | window.onerror = function() { 60 | if (window.ignoreError) { 61 | window.ignoreError = false 62 | return 63 | } 64 | 65 | handleError(formatRuntimerError.apply(null, arguments)) 66 | } 67 | 68 | // 监听资源加载错误(JavaScript Scource failed to load) 69 | window.addEventListener('error', function(event) { 70 | // 过滤 target 为 window 的异常,避免与上面的 onerror 重复 71 | var errorTarget = event.target 72 | if (errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]) { 73 | handleError(formatLoadError(errorTarget)) 74 | } 75 | }, true) 76 | 77 | // 针对 vue 报错重写 console.error 78 | // TODO 79 | console.error = (function(origin) { 80 | return function(info) { 81 | var errorLog = { 82 | type: ERROR_CONSOLE, 83 | desc: info 84 | } 85 | 86 | handleError(errorLog) 87 | origin.call(console, info) 88 | } 89 | })(console.error) 90 | } 91 | 92 | // 处理 try..catch 错误 93 | function handleTryCatchError(error) { 94 | handleError(formatTryCatchError(error)) 95 | } 96 | 97 | /** 98 | * 生成 runtime 错误日志 99 | * 100 | * @param {String} message 错误信息 101 | * @param {String} source 发生错误的脚本 URL 102 | * @param {Number} lineno 发生错误的行号 103 | * @param {Number} colno 发生错误的列号 104 | * @param {Object} error error 对象 105 | * @return {Object} 106 | */ 107 | function formatRuntimerError(message, source, lineno, colno, error) { 108 | return { 109 | type: ERROR_RUNTIME, 110 | desc: message + ' at ' + source + ':' + lineno + ':' + colno, 111 | stack: error && error.stack ? error.stack : 'no stack' // IE <9, has no error stack 112 | } 113 | } 114 | 115 | /** 116 | * 生成 laod 错误日志 117 | * 118 | * @param {Object} errorTarget 119 | * @return {Object} 120 | */ 121 | function formatLoadError(errorTarget) { 122 | return { 123 | type: LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()], 124 | desc: errorTarget.baseURI + '@' + (errorTarget.src || errorTarget.href), 125 | stack: 'no stack' 126 | } 127 | } 128 | 129 | /** 130 | * 生成 try..catch 错误日志 131 | * 132 | * @param {Object} error error 对象 133 | * @return {Object} 格式化后的对象 134 | */ 135 | function formatTryCatchError(error) { 136 | return { 137 | type: ERROR_TRY_CATHC, 138 | desc: error.message, 139 | stack: error.stack 140 | } 141 | } 142 | 143 | /** 144 | * 错误数据预处理 145 | * 146 | * @param {Object} errorLog 错误日志 147 | */ 148 | function handleError(errorLog) { 149 | // 是否延时处理 150 | if (!config.concat) { 151 | !needReport(config.sampling) || config.report([errorLog]) 152 | } else { 153 | pushError(errorLog) 154 | report(errorList) 155 | } 156 | } 157 | 158 | /** 159 | * 往异常信息数组里面添加一条记录 160 | * 161 | * @param {Object} errorLog 错误日志 162 | */ 163 | function pushError(errorLog) { 164 | if (needReport(config.sampling) && errorList.length < config.maxError) { 165 | errorList.push(errorLog) 166 | } 167 | } 168 | 169 | /** 170 | * 设置一个采样率,决定是否上报 171 | * 172 | * @param {Number} sampling 0 - 1 173 | * @return {Boolean} 174 | */ 175 | function needReport(sampling) { 176 | return Math.random() < (sampling || 1) 177 | } 178 | 179 | export default monitor 180 | -------------------------------------------------------------------------------- /src/try.js: -------------------------------------------------------------------------------- 1 | import { 2 | arrayFrom, 3 | isFunction, 4 | merge 5 | } from './util' 6 | 7 | var tryJS = {} 8 | 9 | tryJS.wrap = wrap 10 | tryJS.wrapArgs = tryifyArgs 11 | 12 | var config = { 13 | handleTryCatchError: function() {} 14 | } 15 | 16 | export function setting(opts) { 17 | merge(opts, config) 18 | } 19 | 20 | function wrap(func) { 21 | return isFunction(func) ? tryify(func) : func 22 | } 23 | 24 | /** 25 | * 将函数使用 try..catch 包装 26 | * 27 | * @param {Function} func 需要进行包装的函数 28 | * @return {Function} 包装后的函数 29 | */ 30 | function tryify(func) { 31 | // 确保只包装一次 32 | if (!func._wrapped) { 33 | func._wrapped = function() { 34 | try { 35 | return func.apply(this, arguments) 36 | } catch (error) { 37 | config.handleTryCatchError(error) 38 | window.ignoreError = true 39 | 40 | throw error 41 | } 42 | } 43 | } 44 | 45 | return func._wrapped 46 | } 47 | 48 | /** 49 | * 只对函数参数进行包装 50 | * 51 | * @param {Function} func 需要进行包装的函数 52 | * @return {Function} 53 | */ 54 | function tryifyArgs(func) { 55 | return function() { 56 | var args = arrayFrom(arguments).map(function(arg) { 57 | return wrap(arg) 58 | }) 59 | 60 | return func.apply(this, args) 61 | } 62 | } 63 | 64 | export default tryJS 65 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * debounce 3 | * 4 | * @param {Function} func 实际要执行的函数 5 | * @param {Number} delay 延迟时间,单位是 ms 6 | * @param {Function} callback 在 func 执行后的回调 7 | * 8 | * @return {Function} 9 | */ 10 | export function debounce(func, delay, callback) { 11 | var timer 12 | 13 | return function() { 14 | var context = this 15 | var args = arguments 16 | 17 | clearTimeout(timer) 18 | 19 | timer = setTimeout(function() { 20 | func.apply(context, args) 21 | 22 | !callback || callback() 23 | }, delay) 24 | } 25 | } 26 | 27 | /** 28 | * merge 29 | * 30 | * @param {Object} src 31 | * @param {Object} dest 32 | * @return {Object} 33 | */ 34 | export function merge(src, dest) { 35 | for (var item in src) { 36 | dest[item] = src[item] 37 | } 38 | 39 | return dest 40 | } 41 | 42 | /** 43 | * 是否是函数 44 | * 45 | * @param {Any} func 判断对象 46 | * @return {Boolean} 47 | */ 48 | export function isFunction(func) { 49 | return Object.prototype.toString.call(func) === '[object Function]' 50 | } 51 | 52 | /** 53 | * 将类数组转化成数组 54 | * 55 | * @param {Object} arrayLike 类数组对象 56 | * @return {Array} 转化后的数组 57 | */ 58 | export function arrayFrom(arrayLike) { 59 | return [].slice.call(arrayLike) 60 | } 61 | --------------------------------------------------------------------------------