├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── action.js ├── config.js ├── dependence.js ├── dispatcher.js ├── environment.js ├── index.js ├── legacyDispatcher.js ├── net.js ├── store.js ├── tracker.js ├── userdata.js ├── util.js └── window.js ├── test ├── index.js ├── karma.conf.js └── unit │ ├── dispatcherTest.js │ ├── storeTest.js │ ├── trackerTest.js │ ├── userdataTest.js │ ├── utilTest.js │ └── windowTest.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/*.js 2 | node_modules 3 | *.sh 4 | .eslintrc.js 5 | dict 6 | json.js 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 5, 5 | sourceType: 'script' 6 | }, 7 | "env": { 8 | "es6": false, 9 | "node": true, 10 | "browser": true, 11 | "amd": true, 12 | "commonjs": true, 13 | "jquery": true, 14 | "worker": true, 15 | "mocha": true 16 | }, 17 | "globals":{ 18 | "seajs":true, 19 | "jQuery":true, 20 | "$":true 21 | }, 22 | // add your custom rules here 23 | 'rules': { 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 26 | // 强制使用一致的缩进 27 | "indent":[2, 2, {"VariableDeclarator": 1, "SwitchCase": 1}], 28 | // 要求或禁止使用 Unicode 字节顺序标记 29 | "unicode-bom": 2, 30 | // 强制关键字周围空格的一致性 31 | "keyword-spacing": 2, 32 | // 要求中缀操作符周围有空格 33 | "space-infix-ops": 2, 34 | // 禁止出现多个空格 35 | "no-multi-spaces": 2, 36 | // 要求或禁止在一元操作符之前或之后存在空格 37 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 38 | // 禁止 function 标识符和应用程序之间有空格 39 | "no-spaced-func": 2, 40 | // 要求或禁止语句块之前的空格 41 | "space-before-blocks": [2, "always"], 42 | // 强制行的最大长度 43 | "max-len": [2, 120, { "ignoreComments": true }], 44 | // 强制或禁止分号 45 | "semi": [2, "never"], 46 | // 要求遵循大括号约定 47 | "curly": 2, 48 | // 要求使用骆驼拼写法 TODO 49 | "camelcase": [2, { "properties": "never" }], 50 | // 要求构造函数首字母大写 51 | "new-cap": [2, { "capIsNew": false }], 52 | // 禁用未声明的变量 TODO http://eslint.org/docs/user-guide/configuring#specifying-environments 53 | "no-undef": 2, 54 | // 强制在逗号周围使用空格 55 | "comma-spacing": 2, 56 | // 逗号风格 57 | "comma-style": 2, 58 | // 强制引号风格 59 | "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 60 | // 操作符、换行符 TODO 目前模块字符串连接问题较多 61 | "operator-linebreak": [2, "before"], 62 | // 禁止多行字符串 TODO 目前项目中用的比较多 63 | "no-multi-str": 0, 64 | // 要求一致的 This TODO 好像只能设置一个 65 | "consistent-this": [2, "that", "self", "me"] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ftp.sh 2 | upload.bat 3 | /bundle.* 4 | .idea 5 | !.gitignore 6 | /dist 7 | /test/coverage 8 | node_modules 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Liu Zhen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | ``` 3 | $ npm i 4 | $ npm run build 5 | ``` 6 | 7 | ## 使用 8 | 9 | ``` html 10 | 15 | 16 | 19 | ``` 20 | 21 | token是必须的,这也是服务端区分不同用户的凭据 22 | 23 | 默认的配置如下: 24 | 25 | ``` js 26 | var defaults = { 27 | enabled: true, // 是否可用,关闭以后不再捕获和发送错误信息 28 | action: true, // 是否监控并发送用户操作信息 29 | net: true, // 是否hook ajax请求 30 | dependence: true, // 是否发送页面上的依赖库信息,默认会有几个内置流行库的检测,剩余的通过遍历window对象获取 31 | compress: true, // 是否压缩提交的数据,使用https://github.com/pieroxy/lz-string 整体性价比兼容性都比较靠谱 32 | autoStart: true, // 自动开始,不想自动开始的话就设置成false,然后手动start 33 | report: { 34 | delay: 5000 // 报告发送的延迟时间(单位ms),如果一个时间段内有很多报告产生,那么就放到一起发送 35 | }, 36 | ignoreErrors: [], // 可忽略的错误 37 | ignoreUrls:[], // 可忽略的url,那些产生错误的url 38 | } 39 | ``` 40 | ## Q&A 41 | 42 | 不想自动开始监控?先设置autoStart为false 43 | 44 | ``` js 45 | window.flexTracker = { 46 | token: 'testtoken-123-456-789', 47 | autoStart: false 48 | } 49 | ``` 50 | 51 | 然后,手动开启`window.flexTracker.start()` 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tracker_client", 3 | "version": "0.2.0", 4 | "description": "a flexble client error and action track script", 5 | "main": "src/index.js", 6 | "directories": { 7 | "test": "test", 8 | "src": "src" 9 | }, 10 | "scripts": { 11 | "lint": "eslint src ./src/*.js", 12 | "dev": "cross-env NODE_ENV=development webpack --progress -d", 13 | "dev-watch": "cross-env NODE_ENV=development webpack --progress -d --watch", 14 | "build": "cross-env NODE_ENV=production webpack --progress -p", 15 | "unit": "cross-env NODE_ENV=test karma start test/karma.conf.js --single-run" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git@git.jd.com:liuzhen154/tracker_client.git" 20 | }, 21 | "keywords": [ 22 | "client", 23 | "track" 24 | ], 25 | "author": "catalsdevelop@gmail.com", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "cross-env": "^2.0.1", 29 | "eslint": "^3.4.0", 30 | "isparta-loader": "^2.0.0", 31 | "karma": "^0.13.22", 32 | "karma-coverage": "^0.5.5", 33 | "karma-mocha": "^0.2.2", 34 | "karma-phantomjs-launcher": "^1.0.2", 35 | "karma-should": "^1.0.0", 36 | "karma-sinon": "^1.0.5", 37 | "karma-sourcemap-loader": "^0.3.7", 38 | "karma-spec-reporter": "0.0.26", 39 | "karma-webpack": "^1.8.0", 40 | "mocha": "^3.0.2", 41 | "should": "^11.1.0", 42 | "sinon": "^1.17.5", 43 | "webpack": "^1.13.2", 44 | "webpack-merge": "^0.14.1" 45 | }, 46 | "dependencies": { 47 | "json-js": "^1.1.2", 48 | "lz-string": "^1.4.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 记录页面上用户的操作信息 4 | * 5 | */ 6 | var util = require('./util') 7 | 8 | function action(config, store) { 9 | this.data = [] 10 | this.store = store 11 | if (config.action) { 12 | this.needRecordClickSelectors = ['a', 'button', 'input[button]', 'input[submit]', 'input[radio]', 'input[checkbox]'] 13 | this.needRecordChangeSelectors = ['input[text]', 'input[password]', 'textarea', 'select'] 14 | this.init() 15 | } 16 | } 17 | 18 | action.prototype = { 19 | init: function() { 20 | var clickHandler = util.bind(this.eventHandler, this, 'click', this.needRecordClickSelectors) 21 | var inputHandler = util.bind(this.eventHandler, this, 'input', this.needRecordChangeSelectors) 22 | if (window.addEventListener) { 23 | document.addEventListener('click', clickHandler, true) // 标准浏览器在捕获阶段触发 24 | document.addEventListener('blur', inputHandler, true) 25 | } 26 | else if (window.attachEvent) { 27 | document.attachEvent('onclick', clickHandler) 28 | document.attachEvent('onfocusout', inputHandler) // document内部有元素发生blur就会触发 29 | } 30 | }, 31 | /** 32 | * 页面点击或者时区焦点时触发,该函数绑定了动作 33 | * @param {String} action 'click' or 'input' 34 | * @param {String} selectorFilter 要过滤的标签类型 35 | * @param {Event} evt 事件对象 36 | */ 37 | eventHandler: function(action, selectorFilter, evt) { 38 | var target = evt.target || evt.srcElement 39 | if (target == document || target == window || target == document.documentElement || target == document.body) { 40 | return 41 | } 42 | var tag = target.tagName.toLowerCase() 43 | if (this.accept(target, selectorFilter)) { 44 | this.record(target, tag, action) 45 | } 46 | }, 47 | /** 48 | * 查看某个元素是否在要监控的元素类型列表中 49 | * @param {HTMLElement} element 要检测的元素 50 | * @param {String} selector 元素列表字符串 51 | * @return {Boolean} 检测结果 52 | */ 53 | accept: function(element, selector) { 54 | var tag = element.tagName.toLowerCase() 55 | if (tag === 'input' && element.type) { 56 | tag += '[' + element.type + ']' 57 | } 58 | return util.indexOf(selector, tag) > -1 59 | }, 60 | 61 | /** 62 | * 返回一个元素对应的attributes 63 | * @param {HTMLElement} element 要获取元素的属性 64 | * @return {Object} key-value形式的对象 65 | */ 66 | attributes: function(element) { 67 | var result = {} 68 | var attributes = element.attributes 69 | // 在ie6-7下面,会获取到很多内置的attribute,使用specified属性来区分,在版本浏览器下,都会输出正确的属性,同时specified也是true。不保存非checkbox、radio的value属性的信息 70 | var len = attributes.length 71 | for (var i = 0; i < len; ++i) { 72 | var item = attributes[i] 73 | if (item.specified) { 74 | if (item.name == 'value' && this.accept(element, ['textarea', 'input[text]', 'input[password]'])) { 75 | continue 76 | } 77 | result[item.name] = item.value 78 | } 79 | } 80 | return result 81 | }, 82 | 83 | /** 84 | * 根据不同的元素,记录不同的内容,input[password]不记录value,input[text]和textarea只记录文本长度,input[checkbox]、input[radio]需要记录value和checked属性,select记录选中的value和index 85 | * @param {HTMLElement} target 86 | * @param {String} lowercaseTagName 小写的标签tag 87 | * @param {String} action 动作标示,'click','input' 88 | * @return {String} 成功返回这个log的guid 89 | */ 90 | record: function (element, lowercaseTagName, action) { 91 | var attributes = this.attributes(element) 92 | var log = { 93 | tag: lowercaseTagName, 94 | action: action, 95 | time: util.isoDate(), 96 | attribute: attributes, 97 | extra: {} 98 | } 99 | if (lowercaseTagName === 'input') { 100 | switch (element.type) { 101 | case 'text': 102 | log.extra.length = element.value.length 103 | break 104 | case 'checkbox': 105 | case 'radio': 106 | log.extra.checked = element.checked 107 | break 108 | } 109 | } 110 | else if (lowercaseTagName === 'textarea') { 111 | log.extra.length = element.value.length 112 | } 113 | else if (lowercaseTagName === 'select') { 114 | log.extra.selectedIndex = element.selectedIndex 115 | log.extra.value = element.value 116 | } 117 | return this.store.add(log, 'act') 118 | } 119 | } 120 | 121 | module.exports = action 122 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 配置信息模块 4 | * 5 | */ 6 | 7 | /** 8 | * 内部配置信息 9 | * @type {Object} 10 | */ 11 | var settings = { 12 | version: '0.2.0', // 版本号有助于后端处理不同版本的数据格式,不同版本之前可能会出现数据类型的差异 13 | maxStackDepth: 10, // 错误堆栈最大深度 14 | reportPath: 'www.example.com/catch', // 这是服务器提交的地址,根据自己的情况设置 15 | maxReportCount: 200, // 最大错误数量,超过这个数就不发送了 16 | maxErrorToleranceAge: 1000 // 最大错误允许间隔(单位:ms),小于这个间隔的同类错误将被抛弃 17 | } 18 | 19 | /** 20 | * 默认的配置信息 21 | * @type {Object} 22 | */ 23 | var defaults = { 24 | enabled: true, // 是否可用,关闭以后不再捕获和发送错误信息 25 | action: true, // 是否监控并发送用户操作信息 26 | hook: true, // 是否增加hook,把setTimeout/setInterval/requestAnimationFrame和add/removeEventListener给wrap一层 27 | net: true, // 是否hook ajax请求 28 | dependence: true, // 是否发送页面上的依赖库信息,默认会有几个内置流行库的检测,剩余的通过遍历window对象获取 29 | compress: true, // 是否压缩提交的数据,使用https://github.com/pieroxy/lz-string 整体性价比兼容性都比较靠谱 30 | autoStart: true, // 自动开始,不想自动开始的话就设置成false,然后手动start 31 | report: { 32 | delay: 5000 // 报告发送的延迟时间(单位ms),如果一个时间段内有很多报告产生,那么就放到一起发送 33 | }, 34 | ignoreErrors: [], // 可忽略的错误 35 | ignoreUrls:[], // 可忽略的url,那些产生错误的url 36 | settings: settings 37 | } 38 | 39 | module.exports = defaults 40 | -------------------------------------------------------------------------------- /src/dependence.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 增加依赖检测 4 | * 5 | */ 6 | 7 | var util = require('./util') 8 | 9 | function popLibVersion() { 10 | var arr = [], v 11 | var kvp = [ 12 | ['jQuery', 'jQuery.fn.jquery'], 13 | ['jQuery ui', 'jQuery.ui.version'], 14 | ['lodash(underscore)', '_.VERSION'], 15 | ['Backbone', 'Backbone.VERSION'], 16 | ['knockout', 'ko.version'], 17 | ['Angular', 'angular.version.full'], 18 | ['React', 'React.version'], 19 | ['Vue', 'Vue.version'], 20 | ['Ember', 'Ember.VERSION'], 21 | ['Moment', 'moment.version'], 22 | ['SeaJS', 'seajs.version'] 23 | ] 24 | 25 | for (var i = 0; i < kvp.length; ++i) { 26 | var version = util.globalObjValue(kvp[i][1]) 27 | if (version != null) { 28 | arr.push([kvp[i][0], version]) 29 | } 30 | } 31 | return arr 32 | } 33 | 34 | module.exports = { 35 | all: function() { 36 | var result = [] 37 | var filter = 'jQuery _ Backbone ko angular React Vue Ember moment seajs' 38 | for (var p in window) { 39 | if (filter.indexOf(p) === -1) { 40 | var version = null 41 | var item = window[p] 42 | try { 43 | if (item) { 44 | version = item.version || item.Version || item.VERSION 45 | } 46 | if (version) { 47 | result.push([p, version]) 48 | } 49 | } 50 | catch(e) {} 51 | } 52 | } 53 | return result.concat(popLibVersion()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/dispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 错误信息发送 4 | * 5 | */ 6 | 7 | var util = require('./util') 8 | 9 | // 加载先检测是否https和是否支持cors xmlhttprequest 10 | var https = window.location.protocol.indexOf('https') > -1 ? true : false 11 | 12 | var supportCors = function() { 13 | if (util.isIE67()) { 14 | return false 15 | } 16 | return 'withCredentials' in new XMLHttpRequest 17 | }() 18 | 19 | function dispatcher(config) { 20 | this.config = config 21 | } 22 | 23 | dispatcher.prototype = { 24 | /** 25 | * 根据页面协议选择发送到http还是https 26 | * @return {String} 要发送的地址 27 | */ 28 | endPoint: function() { 29 | return (https ? 'https://' : 'http://') + this.config.settings.reportPath + '?token=' + this.config.token + '&' 30 | }, 31 | sendError: function(info) { 32 | var endPoint = this.endPoint(this.config.token) 33 | var xhr = getXHR(endPoint) 34 | if (util.isString(info)) { 35 | xhr.send(info) 36 | } 37 | else { 38 | xhr.send(JSON.stringify(info)) 39 | } 40 | } 41 | } 42 | 43 | function getXHR(url) { // todo ie6-7直接用form提交请求实现,并且减少提交的数据 44 | var xmlHttp 45 | if (supportCors) { // ie9+, chrome, ff 46 | xmlHttp = new XMLHttpRequest() 47 | } 48 | else if (XDomainRequest) { // ie10- 49 | xmlHttp = new XDomainRequest() 50 | } 51 | else { // ie8- 52 | xmlHttp = null 53 | } 54 | 55 | xmlHttp.open('post', url, true) 56 | // XMLDomainRequest不支持设置setRequestHeader方法 57 | xmlHttp.setRequestHeader && xmlHttp.setRequestHeader('Content-Type', 'text/plain') 58 | return xmlHttp 59 | } 60 | 61 | module.exports = dispatcher 62 | -------------------------------------------------------------------------------- /src/environment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 记录基本的浏览器版本、停留时间等信息 4 | * 5 | */ 6 | 7 | var landOn = new Date() // 尽量早初始化这个函数,确保停留时间准确 8 | var environment = { 9 | all: function() { 10 | return { 11 | vw: (document.documentElement ? document.documentElement.clientWidth : document.body.clientWidth), 12 | vh: (document.documentElement ? document.documentElement.clientHeight : document.body.clientHeight), 13 | // ua: navigator.userAgent, 14 | age: new Date() - landOn 15 | } 16 | } 17 | } 18 | 19 | module.exports = environment 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 入口文件 4 | * 5 | */ 6 | require('json-js/json2.js') 7 | 8 | var util = require('./util') 9 | var defaultSettings = require('./config') 10 | var store = require('./store') 11 | var Userdata = require('./userdata') 12 | var Net = require('./net') 13 | var Dispatcher = require('./dispatcher') 14 | var Tracker = require('./tracker') 15 | var LegacyDispatcher = require('./legacyDispatcher') 16 | var Action = require('./action') 17 | var win = require('./window') 18 | 19 | function main(config) { 20 | var tracker = new ShadowTracker(config) 21 | var started = false 22 | var obj = { 23 | start: function() { 24 | if (started) { 25 | util.cw('tracker has started') 26 | return 27 | } 28 | if (config.enabled) { 29 | tracker.init() 30 | started = true 31 | } 32 | }, 33 | config: config, 34 | /** 35 | * 一个简单的设置和获取userdata的方法 36 | * userdata()是获取所有的userdata 37 | * userdata(key) 获取某个key对应的data 38 | * userdata(key, val) 设置某个key对应的data 39 | * 40 | */ 41 | userdata: function() { 42 | var argLength = arguments.length 43 | if (argLength == 0) { 44 | return tracker.userdata.get() 45 | } 46 | else if (argLength == 1) { 47 | var arg = arguments[0] 48 | if (util.isString(arg)) { 49 | return tracker.userdata.get(arg) 50 | } 51 | } 52 | else { 53 | tracker.userdata.set(arguments[0], arguments[1]) 54 | } 55 | } 56 | } 57 | 58 | if (config.autoStart) { 59 | obj.start() 60 | } 61 | 62 | return obj 63 | } 64 | 65 | function ShadowTracker(cfg) { 66 | this.config = cfg 67 | this.userdata = new Userdata(cfg.userdata) 68 | 69 | this.actionMonitor = new Action(this.config, store) 70 | // 初始化不同的上报机制,主要是解决低版本浏览器不支持ajax跨域提交的问题 71 | this.dispatcher = util.isIE67() ? new LegacyDispatcher(this.config) : new Dispatcher(this.config) 72 | this.tracker = new Tracker(this.config, store, this.dispatcher, this.userdata, this.actionMonitor) 73 | } 74 | 75 | ShadowTracker.prototype = { 76 | init: function() { 77 | win.monitor(this.tracker) 78 | new Net(this.config, store, this.tracker) 79 | }, 80 | 81 | /** 82 | * 立刻执行一个函数,并自动捕获函数中出现的异常。 83 | * @param {Function} func 需要执行的函数 84 | * @param {Object} self 函数中的this需要指定的对象 85 | * @param {Params} params 不定个数的参数,作为要指定函数的实参传递进去,如果func是一个匿名函数或者无参函数,则不需要 86 | * @return {Mix} func的返回值 87 | */ 88 | capture: function(func, self, params) { //eslint-disable-line 89 | util.cw('not realized :(') 90 | return 91 | }, 92 | 93 | /** 94 | * 包装一个函数或者对象,主动捕获所有函数或者对象方法的异常 95 | * @param {[type]} func [description] 96 | * @return {[type]} [description] 97 | */ 98 | watch: function(func) { 99 | util.cw('not realized :(') 100 | return 101 | } 102 | } 103 | 104 | // 需要先提供一个全局变量window.flexTracker = {}, 对象内容是配置信息 105 | // window.flexTracker = { 106 | // token: 'token123-456-789', 107 | // net: true, 108 | // userdata: { 109 | // platform: 'desktop', 110 | // release: 12 111 | // }, 112 | // compress: false 113 | // } 114 | if (util.isPlainObject(window.flexTracker)) { 115 | var userConfig = window.flexTracker 116 | if (!userConfig.token) { 117 | util.cw('token is must needed') 118 | } 119 | else { 120 | var config = Object.assign(defaultSettings, userConfig) 121 | window.flexTracker = main(config) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/legacyDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 针对不支持ajax 跨域提交的dispatcher 4 | * 5 | */ 6 | 7 | var util = require('./util') 8 | var dispatcher = require('./dispatcher') 9 | 10 | function object(o) { 11 | var F = function () {} 12 | F.prototype = o 13 | return new F() 14 | } 15 | 16 | function LegacyDispacther(config) { 17 | dispatcher.call(this, config) 18 | } 19 | LegacyDispacther.prototype = object(dispatcher.prototype) 20 | LegacyDispacther.prototype.constructor = LegacyDispacther 21 | 22 | 23 | var queue = [] 24 | function autoSend(url, method) { 25 | if (!!document.body) { 26 | var item 27 | while (item = queue.shift()) { 28 | iframePost(url, method, item) 29 | } 30 | } 31 | else { 32 | setTimeout(util.bind(autoSend, null, url, method), 20) 33 | } 34 | } 35 | 36 | function iframePost(url, method, data) { 37 | var iframe = document.createElement('iframe') 38 | iframe.name = 'framePost-' + util.guid() 39 | iframe.style.display = 'none' 40 | document.body.appendChild(iframe) 41 | iframe.contentWindow.name = iframe.name 42 | 43 | var form = document.createElement('form') 44 | form.enctype = 'application/x-www-form-urlencoded' 45 | form.action = url 46 | form.method = method 47 | form.target = iframe.name 48 | var input = document.createElement('input') 49 | input.name = 'info' 50 | input.type = 'hidden' 51 | if (util.isString(data)) { 52 | input.value = data 53 | } 54 | else { 55 | input.value = JSON.stringify(data) 56 | } 57 | form.appendChild(input) 58 | document.body.appendChild(form) 59 | 60 | iframe.attachEvent('onload', function() { 61 | iframe.detachEvent('onload', arguments.callee) 62 | document.body.removeChild(form)//todo 这里会有leak么? 63 | form = null 64 | document.body.removeChild(iframe) 65 | iframe = null 66 | }) 67 | 68 | form.submit() 69 | } 70 | 71 | LegacyDispacther.prototype.sendError = function(info) { 72 | var endPoint = this.endPoint(this.config.token) 73 | queue.push(info) 74 | autoSend(endPoint, 'post') 75 | } 76 | 77 | module.exports = LegacyDispacther 78 | -------------------------------------------------------------------------------- /src/net.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 捕获ajax请求中的参数 3 | */ 4 | 5 | var util = require('./util') 6 | 7 | var Net = function(config, store, tracker) { 8 | this.config = config 9 | this.store = store 10 | this.tracker = tracker 11 | if (this.config.net) { 12 | this.init() 13 | } 14 | } 15 | 16 | Net.prototype = { 17 | init: function() { 18 | if (window.XMLHttpRequest) { 19 | this.hook(window.XMLHttpRequest) 20 | } 21 | if (window.XDomainRequest) { 22 | this.hook(window.XDomainRequest) 23 | } 24 | }, 25 | hook: function(klass) { 26 | var open = klass.prototype.open 27 | var send = klass.prototype.send 28 | var self = this 29 | // 重写open和send,获取需要的参数 30 | klass.prototype.open = function(method, url) { 31 | this._track = { //直接绑到xhr上 32 | method: method.toLowerCase(), 33 | url: url 34 | } 35 | return open.apply(this, arguments) 36 | } 37 | 38 | klass.prototype.send = function() { 39 | this._track.id = self.store.add({ 40 | start: util.isoDate(), 41 | method: this._track.method, 42 | url: this._track.url 43 | }, 'net') 44 | 45 | self.registerComplete(this) // this = xhr 46 | return send.apply(this, arguments) 47 | } 48 | }, 49 | registerComplete: function(xhr) { 50 | var self = this 51 | 52 | if (xhr.addEventListener) { 53 | xhr.addEventListener('readystatechange', function() { 54 | if (xhr.readyState == 4) { 55 | self.checkComplete(xhr) 56 | } 57 | }, true) 58 | } 59 | else { 60 | setTimeout(function() { 61 | var onload = xhr.onload 62 | xhr.onload = function () { 63 | self.checkComplete(xhr) 64 | return onload.apply(xhr, arguments) 65 | } 66 | 67 | var onerror = xhr.onerror 68 | xhr.onerror = function () { 69 | self.checkComplete(xhr) 70 | return onerror.apply(xhr, arguments) 71 | } 72 | }, 0) 73 | } 74 | }, 75 | checkComplete: function(xhr) { 76 | if (xhr._track) { 77 | var track = this.store.get(xhr._track.id) 78 | if (track) { 79 | var info = track.value 80 | info.finish = util.isoDate() 81 | // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request 82 | info.statusCode = (xhr.status == 1223 ? 204 : xhr.status) 83 | info.statusText = (xhr.status == 1223 ? 'No Content' : xhr.statusText) 84 | 85 | if (xhr.status >= 400 && xhr.status != 1223) { // 如果发现的xhr的错误,就上报,但是这条数据不出现在日志中,只作为错误上报 86 | this.store.remove(xhr._track.id) 87 | this.tracker['catch'](info, 'xhr') 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | module.exports = Net 95 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 存取一些临时数据的仓库 4 | * 5 | */ 6 | 7 | var util = require('./util') 8 | var data = [] 9 | 10 | var store = { 11 | capacity: 20, 12 | 13 | /** 14 | * 添加数据入库 15 | * @param {Mix} value 任何类型的数据 16 | * @param {String} type 任何类型的字符串,用来在all()函数里面做筛选 17 | */ 18 | add: function(value, type) { 19 | var guid = util.guid() 20 | data.push({ 21 | id: guid, 22 | type: type, 23 | value: value 24 | }) 25 | this.truncate() 26 | return guid 27 | }, 28 | 29 | /** 30 | * 根据guid返回对应的数据,找不到返回null 31 | * @param {String} guid 32 | * @return {Mix} 33 | */ 34 | get: function(guid) { 35 | for (var i = 0; i < data.length; ++i) { 36 | var item = data[i] 37 | if (item.id === guid) { 38 | return {key: item.id, value: item.value} 39 | } 40 | } 41 | return null 42 | }, 43 | /** 44 | * 清空数据,这时获取all()得到的是一个空数组 45 | * @param {String} type 清空的类型 46 | */ 47 | clear: function(type) { 48 | var argLength = arguments.length 49 | switch (argLength) { 50 | case 0: 51 | data.length = [] 52 | break 53 | case 1: 54 | var i = 0, item 55 | while (item = data[i++]) { 56 | if (item.type == type) { 57 | data.splice(item, 1) 58 | } 59 | } 60 | break 61 | } 62 | }, 63 | /** 64 | * 保持库内数组的长度,超出的数据会被删除 65 | */ 66 | truncate: function() { 67 | if (data.length <= this.capacity) { 68 | return 69 | } 70 | data.splice(this.capacity) 71 | }, 72 | /** 73 | * 根据指定的guid删除数据 74 | * @param {String} guid 75 | * @return {Boolean} 删除成功返回true,失败返回false 76 | */ 77 | remove: function(guid) { 78 | var len = data.length 79 | for (var i = 0; i < len; ++i) { 80 | var item = data[i] 81 | if (item.id === guid) { 82 | data.splice(i, 1) 83 | return true 84 | } 85 | } 86 | return false 87 | }, 88 | /** 89 | * 根据type返回对应的数据类型,不存在的类型返回空数组,不提供type的话,返回所有数据 90 | * @params {String} type 91 | * @params {Boolean} isSimple 如果是简单类型,直接返回包含value值的数组 92 | * @return {Mix} 93 | */ 94 | all: function(type, isSimple) { 95 | var result = [] 96 | isSimple = isSimple || false 97 | var len = data.length 98 | for (var i = 0; i < len; ++i) { 99 | var item = data[i] 100 | if (!type || item.type === type) { 101 | if (isSimple) { 102 | result.push(item.value) 103 | } 104 | else { 105 | result.push({ 106 | key:item.id, 107 | value:item.value 108 | }) 109 | } 110 | } 111 | } 112 | return result 113 | } 114 | } 115 | 116 | module.exports = store 117 | -------------------------------------------------------------------------------- /src/tracker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 处理异常模块 4 | * 5 | */ 6 | 7 | var util = require('./util') 8 | var env = require('./environment') 9 | var dependenceMointer = require('./dependence') 10 | var lzString = require('lz-string') 11 | 12 | /** 13 | * 根据堆栈的长度,截断多余的堆栈信息 14 | * @param {String} stack 原始堆栈 15 | * @param {Integer} maxDepth 最大堆栈长度 16 | * @return {String} 截断后的堆栈 17 | */ 18 | function subStack(stack, maxDepth) { 19 | if (!stack) { 20 | return null 21 | } 22 | 23 | var arr = stack.toString().split('\n') 24 | var stackDepth = arr.length 25 | if (stackDepth > maxDepth) { 26 | return arr.slice(0, maxDepth).join('\n') 27 | } 28 | return stack 29 | } 30 | 31 | function getErrorKey(error) { 32 | return [error.message, error.file || 'null', error.line || 'null', error.col || 'null'].join('---') 33 | } 34 | 35 | /** 36 | * 异常信息处理器 37 | * @param {Object} cfg 配置信息 38 | * @param {Object} store 存储库 39 | * @param {Object} dispatcher 发送器 40 | * @param {Object} userdata 自定义数据 41 | * @param {Object} actionMonitor 行为捕捉器 42 | */ 43 | var tracker = function(cfg, store, dispatcher, userdata, actionMonitor) { 44 | this.config = cfg 45 | this.store = store 46 | this.dispatcher = dispatcher 47 | this.userdata = userdata 48 | this.action = actionMonitor 49 | this.reportCount = 0 // 已经发送的错误数量 50 | this.lastError = null // 上一个出错信息,{key:{message}--{file}--{line}--{column},time:} 51 | } 52 | 53 | tracker.prototype = { 54 | /** 55 | * 捕获某个类型的异常,处理并发送请求 56 | * @param {Object} error 错误信息 57 | * @param {String} type 异常类型,有window,xhr,catch三种,分别对应window.onerror捕获,hook xhr捕获和主动try,catch捕获 58 | */ 59 | 'catch': function(error, type) { 60 | type = type.toLowerCase() 61 | var info = { 62 | source: type, 63 | environment: env.all(), 64 | url: location.href, 65 | time: util.isoDate(), 66 | token: this.config.token 67 | } 68 | 69 | switch (type) { 70 | case 'window': 71 | error.stack = subStack(error.stack, this.config.settings.maxStackDepth) 72 | info.error = error 73 | break 74 | case 'xhr': 75 | info.error = error 76 | break 77 | case 'catch': // todo 手动触发 78 | break 79 | } 80 | 81 | this.report(info) 82 | }, 83 | /** 84 | * 发送错误报告,对要发送的内容有相应的的筛选规则 85 | * @param {Object} info 报告主体 86 | */ 87 | report: function(info) { 88 | if (this.throttle()) { 89 | return 90 | } 91 | 92 | var key = getErrorKey(info.error) 93 | if (this.lastError && key == this.lastError.key) { // 对发送的错误做一定的筛选 94 | var lastErrorReportTime = new Date(this.lastError.time) // todo 查看ie6-7下面是否支持iso格式的时间格式化 95 | var timespan = new Date() - lastErrorReportTime 96 | if (timespan <= this.config.settings.maxErrorToleranceAge) { 97 | this.store.clear() 98 | return 99 | } 100 | } 101 | 102 | // 加载额外的信息,verison/userdata必带,operation看配置需求 103 | info.userdata = this.userdata.get() 104 | info.version = this.config.settings.version 105 | var cfg = this.config 106 | if (cfg.action) { 107 | Object.assign(info, {operation: this.store.all('act', true)}) 108 | } 109 | if (cfg.dependence) { 110 | Object.assign(info, {dependence: dependenceMointer.all()}) 111 | } 112 | if (cfg.net) { 113 | Object.assign(info, {net: this.store.all('net', true)}) 114 | } 115 | // dev模式下,总是不压缩 116 | if (process.env !== 'development' && cfg.compress) { 117 | info = lzString.compress(JSON.stringify(info)) 118 | } 119 | this.store.clear() 120 | 121 | if (process.env === 'development') { 122 | var oriInfo = JSON.stringify(info) 123 | var start = new Date() 124 | var compressInfo = lzString.compress(oriInfo) 125 | var spend = new Date() - start 126 | console.log(info, oriInfo.length, compressInfo.length, spend) 127 | } 128 | 129 | this.dispatcher.sendError(info) 130 | this.lastError = { 131 | key:key, 132 | time:info.time 133 | } 134 | }, 135 | /** 136 | * 确保别让错误一直发,有时候会遇到错误一直发生的情况,这种情况下就别发了 137 | */ 138 | throttle: function() { 139 | this.reportCount++ 140 | if (this.reportCount > this.config.settings.maxReportCount) { 141 | return true 142 | } 143 | return false 144 | } 145 | } 146 | 147 | module.exports = tracker 148 | -------------------------------------------------------------------------------- /src/userdata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 保存用户自定义数据类型 4 | * 5 | */ 6 | 7 | var util = require('./util') 8 | 9 | function userdata(data) { 10 | this.data = data || {} 11 | } 12 | 13 | userdata.prototype = { 14 | /** 15 | * 设置配置信息 16 | * @param {Mix} params 参数有2种情况,不满足条件的都会被discard 17 | * 18 | * 1. {Object} params 直接将参数对象合并到配置中 19 | * 20 | * 2. {String} p1 要设置的key 21 | * {Mix} p2 对应的数值 22 | */ 23 | set: function() { 24 | var argLength = arguments.length 25 | 26 | switch (argLength) { 27 | case 1: 28 | if (util.isObject(arguments[0])) { 29 | Object.assign(this.data, arguments[0]) 30 | } 31 | break 32 | case 2: 33 | this.data[arguments[0]] = arguments[1] 34 | break 35 | } 36 | }, 37 | 38 | /** 39 | * 根据key返回value值 40 | * @param {String} key 41 | * @return {Mix} 与key对应的配置信息,没有的话返回null 42 | */ 43 | get: function(key) { 44 | if (arguments.length == 0) { 45 | return this.data 46 | } 47 | return this.data[key] || null 48 | }, 49 | 50 | /** 51 | * 删除key对应的数据 52 | * @param {String} key 53 | */ 54 | remove: function(key) { 55 | delete this.data[key] 56 | }, 57 | 58 | /** 59 | * 清空所有数据 60 | */ 61 | clear: function() { 62 | this.data = {} 63 | } 64 | } 65 | 66 | module.exports = userdata 67 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | // 基本的辅助函数 2 | var obj = {} 3 | var types = ['Array', 'Boolean', 'Date', 'Number', 'Object', 'RegExp', 'String', 'Error', 'Window', 'HTMLDocument'] 4 | // 增加判断几种类型的方法 5 | for (var i = 0, c; c = types[i]; ++i) { 6 | obj['is' + c] = (function(type) { 7 | return function(obj) { 8 | return Object.prototype.toString.call(obj) == '[object ' + type + ']' 9 | } 10 | })(c) 11 | } 12 | 13 | /** 14 | * 判断是否简单对象类型对象直接量和new Object初始化的对象 15 | * @param {Object} o 要判断的对象 16 | * @return {Boolean} 17 | */ 18 | function isPlainObject(o) { 19 | var ctor, prot 20 | 21 | if (obj.isObject(o) === false) { 22 | return false 23 | } 24 | 25 | // 是否函数 26 | ctor = o.constructor 27 | if (typeof ctor !== 'function') { 28 | return false 29 | } 30 | // 原型是否对象 31 | prot = ctor.prototype 32 | if (obj.isObject(prot) === false) { 33 | return false 34 | } 35 | 36 | if (prot.hasOwnProperty('isPrototypeOf') === false) { 37 | return false 38 | } 39 | 40 | return true 41 | } 42 | 43 | // 增加简单的bind和extend 44 | function bind(method, self) { //eslint-disable-line 45 | var slice = Array.prototype.slice 46 | var args = slice.call(arguments, 2) 47 | return function() { 48 | var innerArgs = slice.call(arguments) 49 | return method.apply(self, args.concat(innerArgs)) 50 | } 51 | } 52 | 53 | /** 54 | * 判断是否ie6-8 http://www.cnblogs.com/rubylouvre/archive/2010/01/28/1658006.html 55 | * @return {Boolean} 56 | */ 57 | function isOldIE() { 58 | return !+[1,] // eslint-disable-line 59 | } 60 | 61 | /** 62 | * 判断是否ie浏览器 63 | * @return {Boolean} 64 | */ 65 | function isIE() { 66 | return 'ActiveXObject' in window 67 | } 68 | 69 | /** 70 | * 判断是否ie67 71 | * @return {Boolean} 72 | */ 73 | function isIE67() { 74 | var ua = navigator.userAgent 75 | var isIE6 = ua.search(/MSIE 6/i) != -1 76 | var isIE7 = ua.search(/MSIE 7/i) != -1 77 | return isIE6 || isIE7 78 | } 79 | 80 | 81 | /** 82 | * 左侧补零 83 | * @param {Integer} num 需要补零的数值 84 | * @param {Integer} n 补充的位数 85 | * @return {String} 补零后的字符串 86 | */ 87 | function pad(num, n) { 88 | var len = String(num).length 89 | while (len < n) { 90 | num = '0' + num 91 | len++ 92 | } 93 | return num 94 | } 95 | 96 | /** 97 | * console.warn的简化版,如果浏览器不支持console,那么无需输出 98 | * @param {String} message 要输出的警告信息 99 | */ 100 | function cw(message) { 101 | console && console.warn(message) 102 | } 103 | 104 | /** 105 | * 返回全局的对象值,有值的话返回。 106 | * @param {[String]} namespace 名字空间 107 | * @return {[Mix]} 108 | */ 109 | function globalObjValue(namespace) { 110 | try { 111 | return eval(namespace) 112 | } 113 | catch (e) { 114 | return null 115 | } 116 | } 117 | 118 | function addEvent(element, type, listener, capture) { 119 | if (window.addEventListener) { 120 | element.addEventListener(type, listener, capture || false) 121 | } 122 | else if (window.attachEvent) { 123 | element.attachEvent('on' + type, listener) 124 | } 125 | // 如果还要处理dom1 event类型,需要缓存用户原来设置的事件 126 | } 127 | 128 | function removeEvent(element, type, listener, capture) { 129 | if (window.removeEventListener) { 130 | element.removeEventListener(type, listener, capture || false) 131 | } 132 | else if (window.detachEvent) { 133 | element.detachEvent('on' + type, listener) 134 | } 135 | } 136 | 137 | /** 138 | * 返回带时区的iso8601格式的utc时间 139 | * @return {[String]} 140 | */ 141 | function getISODateNow() { 142 | var now = new Date() 143 | var timezone = -now.getTimezoneOffset() // 这个单位是分钟,值是反的 144 | var tzStr = timezone >= 0 ? '+' : '-' 145 | tzStr += pad((timezone / 60), 2) + ':' + pad((timezone % 60), 2) 146 | 147 | var dateWithTimezone = new Date(now - 0 + timezone * 60 * 1000) 148 | return dateWithTimezone.getUTCFullYear() 149 | + '-' + pad( dateWithTimezone.getUTCMonth() + 1, 2) 150 | + '-' + pad( dateWithTimezone.getUTCDate(), 2) 151 | + 'T' + pad( dateWithTimezone.getUTCHours(), 2) 152 | + ':' + pad( dateWithTimezone.getUTCMinutes(), 2) 153 | + ':' + pad( dateWithTimezone.getUTCSeconds(), 2) 154 | + tzStr 155 | } 156 | 157 | /** 158 | * Array.prototype.indexOf,不支持元素是复杂类型的情况,只是通过===来判断 159 | * @param arr 数组 160 | * @param val 检测值 161 | * @returns {number} 所在下标,没有返回-1 162 | */ 163 | function indexOf(arr, val) { 164 | if (isOldIE()) { 165 | for (var i = 0; i < arr.length; ++i) { 166 | if (arr[i] === val) { 167 | return i 168 | } 169 | } 170 | return -1 171 | } 172 | else { 173 | return Array.prototype.indexOf.call(arr, val) 174 | } 175 | } 176 | 177 | 178 | /** 179 | * 返回一个伪随机guid 参看http://www.jb51.net/article/40678.htm,跑了几个不同的实现,采取效率高一些的实现 180 | * @return {String} 181 | */ 182 | function guid() { 183 | return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4()) 184 | } 185 | function S4() { 186 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1) 187 | } 188 | 189 | /** 190 | * 把一个对象的key-value转化成querystring的形式 191 | * @param 要转换的对象 192 | * @returns {string} 转换后的字符串,会在末尾添加'&' 193 | */ 194 | function object2Query(obj) { 195 | var data = [] 196 | for (var p in obj) { 197 | if (obj.hasOwnProperty(p)) { 198 | data.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])) 199 | } 200 | } 201 | return data.join('&') + '&' 202 | } 203 | // Object.assign polyfill加入 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 204 | if (typeof Object.assign != 'function') { 205 | Object.assign = function(target) { 206 | 'use strict' 207 | if (target == null) { 208 | throw new TypeError('Cannot convert undefined or null to object') 209 | } 210 | 211 | target = Object(target) 212 | for (var index = 1; index < arguments.length; index++) { 213 | var source = arguments[index] 214 | if (source != null) { 215 | for (var key in source) { 216 | if (Object.prototype.hasOwnProperty.call(source, key)) { 217 | target[key] = source[key] 218 | } 219 | } 220 | } 221 | } 222 | return target 223 | } 224 | } 225 | 226 | var output = Object.assign(obj, { 227 | isPlainObject: isPlainObject, 228 | bind: bind, 229 | isOldIE: isOldIE, 230 | isIE: isIE, 231 | isIE67: isIE67, 232 | pad: pad, 233 | cw: cw, 234 | guid: guid, 235 | addEvent: addEvent, 236 | removeEvent: removeEvent, 237 | indexOf: indexOf, 238 | isoDate: getISODateNow, 239 | object2Query: object2Query, 240 | globalObjValue: globalObjValue 241 | }) 242 | 243 | module.exports = output 244 | -------------------------------------------------------------------------------- /src/window.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description window.onerror上绑定异常监控 4 | * 5 | */ 6 | var util = require('./util') 7 | /** 8 | * 负责绑定window上的全局错误,对于无法处理的异常会自动捕获,但是要注意[跨域问题](http://blog.bugsnag.com/script-error)。 9 | * @param {Object} tracker 错误采集对象 10 | */ 11 | function errorBinder(tracker) { 12 | // 不使用addEventListener,保证兼容性,但是要确保之前绑定的onerror事件可以正常运行。 13 | var oldOnError = window.onerror 14 | var slice = Array.prototype.slice 15 | window.onerror = function(message, file, line, column, innerError) { 16 | // 后4个参数,在跨域异常的时候,会有不同的数据。 17 | // file 在ie系列下面永远有数据,chrome非cors没有数据,ff都有 18 | // line 非cors都没有,但是ie通过window.event可以获取 19 | // column ie没有这个参数,其他同上 20 | // innerRrror ie下没有,chrome只有.name .message .stack三个属性,ff下还包含.fileName .lineNumber .columnNumber额外三个属性,跨域策略同上 21 | var args = slice.call(arguments) 22 | if (oldOnError) { 23 | oldOnError.apply(window, args) 24 | } 25 | // var stack = []; 26 | // var f = arguments.callee.caller; 27 | // while (f) { 28 | // stack.push(f.name); 29 | // f = f.caller; 30 | // } 31 | // console.log(message, "from", stack); 32 | var error = flatError.apply(window, args) 33 | // 如果这个错误只有'Script error.',连file信息都没有,视为无用信息,抛弃 34 | if (error.message.indexOf('Script error') > -1 && error.file === null) { 35 | return false 36 | } 37 | tracker['catch'](error, 'window') 38 | return false // 确保控制台可以显示错误 39 | } 40 | } 41 | 42 | /** 43 | * 抹平不同浏览器下面全局报错的属性值,尽可能多的提供错误信息 44 | * @param {[type]} message 错误信息 45 | * @param {[type]} file 错误发生的文件路径 46 | * @param {[type]} line 行号 47 | * @param {[type]} column 列号 48 | * @param {[type]} innerError 对应的错误 49 | * @return {[type]} 抹平后的错误数据对象 50 | */ 51 | function flatError(message, file, line, column, innerError) { 52 | // ie10-全部通过window.event获取 todo ie10+需要验证是否和ie10-一样 53 | var stack = null 54 | 55 | if (util.isIE()) { 56 | var evt = window.event 57 | message = message || evt.errorMessage 58 | file = file || evt.errorUrl 59 | line = line || evt.errorLine 60 | column = column || evt.errorCharacter 61 | } 62 | else { 63 | file = file || (innerError && innerError.fileName) || null 64 | line = line || (innerError && innerError.lineNumber) || null 65 | column = column || (innerError && innerError.columnNumber) || null 66 | stack = (innerError && innerError.stack) || null 67 | } 68 | 69 | return { 70 | message: message, 71 | file: file, 72 | line: line, 73 | column: column, 74 | stack: stack 75 | } 76 | } 77 | 78 | var output = { 79 | monitor: errorBinder 80 | } 81 | 82 | module.exports = output 83 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // http://webpack.github.io/docs/context.html#require-context 2 | // https://github.com/webpack/karma-webpack 3 | 4 | var testsContext = require.context('./unit', true, /Test\.js$/) 5 | testsContext.keys().forEach(testsContext) 6 | 7 | var srcContext = require.context('../src', true, /\.js$/) 8 | srcContext.keys().forEach(srcContext) 9 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpackConfig = require('../webpack.config') 3 | 4 | delete webpackConfig.entry 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | browsers: ['PhantomJS'], 9 | frameworks: ['mocha', 'should', 'sinon'], 10 | reporters: ['spec', 'coverage'], 11 | files:['./index.js'], 12 | preprocessors: { 13 | './index.js': ['webpack', 'sourcemap'] 14 | }, 15 | webpack: webpackConfig, 16 | webpackMiddleware : { 17 | noInfo: true 18 | }, 19 | coverageReporter: { 20 | dir: './coverage', 21 | reporters:[ 22 | {type: 'lcov', subdir: '.'}, 23 | {type: 'text-summary'} 24 | ] 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /test/unit/dispatcherTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description dispatcher模块测试 4 | * 5 | */ 6 | var config = require('../../src/config'); 7 | config.token = 'token-123-456-789'; 8 | 9 | var disp = require('../../src/dispatcher'); 10 | var dispatcher = new disp(config); 11 | 12 | describe('dispatcher module test', function(){ 13 | describe('.endPoint()', function(){ 14 | it('返回正常的远程接口http/https地址', function(){ 15 | dispatcher.endPoint().should.eql('http://www.example.com/catch?token=token-123-456-789&'); 16 | }); 17 | }); 18 | 19 | describe('.sendError()', function(){ 20 | var newXhr,requests; 21 | beforeEach(function(){ 22 | newXhr = sinon.useFakeXMLHttpRequest(); 23 | requests = []; 24 | newXhr.onCreate = function(xhr){ 25 | requests.push(xhr); 26 | } 27 | }); 28 | 29 | afterEach(function(){ 30 | sinon.restore(); 31 | }); 32 | 33 | it('发送正确的错误信息到服务器', function(){ 34 | var data = { 35 | name:'liuzhen7', 36 | age:30, 37 | detail:{ 38 | married:true, 39 | children:2 40 | } 41 | }; 42 | dispatcher.sendError(data); 43 | requests.length.should.eql(1); 44 | var request = requests[0]; 45 | request.method.toLowerCase().should.eql('post'); 46 | request.requestBody.should.eql("{\"name\":\"liuzhen7\",\"age\":30,\"detail\":{\"married\":true,\"children\":2}}"); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/unit/storeTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description 公共存储功能模块测试 4 | * 5 | */ 6 | var store = require('../../src/store'); 7 | 8 | describe('store module test', function(){ 9 | var old; 10 | beforeEach(function(){ 11 | old = Math.random; 12 | }); 13 | 14 | afterEach(function(){ 15 | Math.random = old; 16 | store.clear(); 17 | }); 18 | 19 | describe('.add()',function () { 20 | it('添加一项数据,并返回guid', function (){ 21 | Math.random = function(){ 22 | return .1234567;//1f9a1f9a-1f9a-1f9a-1f9a-1f9a1f9a1f9a 23 | } 24 | for(var i = 0; i < 8; ++i){ 25 | var id = store.add({action:'input', element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a'); 26 | id.should.eql('1f9a1f9a-1f9a-1f9a-1f9a-1f9a1f9a1f9a'); 27 | } 28 | 29 | Math.random = function(){ 30 | return .7654321;//c3f3c3f3-c3f3-c3f3-c3f3-c3f3c3f3c3f3 31 | } 32 | for(i= 0; i < 8; ++i){ 33 | var id = store.add({action:'click', element:{type:'checkbox', checked:true}, tag:'input'}, 'a'); 34 | id.should.eql('c3f3c3f3-c3f3-c3f3-c3f3-c3f3c3f3c3f3'); 35 | } 36 | 37 | Math.random = function(){ 38 | return .13579;//22c322c3-22c3-22c3-22c3-22c322c322c3 39 | } 40 | for(i= 0; i < 8; ++i){ 41 | var id = store.add({url:'http://localhost:8000', method:'GET', statusCode:200, responseText:'{name:"liuzhen7",id:"777"}'}, 'xhr'); 42 | id.should.eql('22c322c3-22c3-22c3-22c3-22c322c322c3'); 43 | } 44 | store.all().length.should.eql(store.capacity); 45 | }) 46 | }); 47 | 48 | describe('.get()', function(){ 49 | var id1,id2,id3,id4,id5; 50 | 51 | beforeEach(function(){ 52 | id1 = store.add({action:'input', idx:1, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a1'); 53 | id2 = store.add({action:'input', idx:2, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a1');//d 54 | id3 = store.add({action:'input', idx:3, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a2');//d 55 | id4 = store.add({action:'input', idx:4, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a2');//d 56 | id5 = store.add({action:'input', idx:5, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a2'); 57 | }); 58 | 59 | it('guid存在时,返回对应的项', function(){ 60 | var item2 = store.get(id2); 61 | item2.should.eql({ 62 | key:id2, 63 | value:{action:'input', idx:2, element:{type:'text', autocomplete:true}, tag:'input',length:24} 64 | }); 65 | }); 66 | 67 | it('guid不存在时,返回null', function(){ 68 | var itemNull = store.get('notexistname'); 69 | should(itemNull).be.exactly(null); 70 | }); 71 | }); 72 | 73 | describe('.remove()', function(){ 74 | var id1, id2, id3, id4, id5; 75 | before(function(){ 76 | id1 = store.add({action:'input', idx:1, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a1'); 77 | id2 = store.add({action:'input', idx:2, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a1');//d 78 | id3 = store.add({action:'input', idx:3, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a2');//d 79 | id4 = store.add({action:'input', idx:4, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a2');//d 80 | id5 = store.add({action:'input', idx:5, element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a2'); 81 | }); 82 | 83 | it('删除与key对应的项', function(){ 84 | var result1 = store.remove(id2); 85 | result1.should.be.true(); 86 | store.all().length.should.eql(4); 87 | store.all('a1').length.should.eql(1); 88 | 89 | var result2 = store.remove(id3); 90 | result2.should.be.true(); 91 | store.all().length.should.eql(3); 92 | store.all('a2').length.should.eql(2); 93 | 94 | var result3 = store.remove(id4); 95 | result3.should.be.true(); 96 | var all = store.all(); 97 | all.length.should.eql(2); 98 | store.all('a2').length.should.eql(1); 99 | 100 | var result4 = store.remove('not-exist-id'); 101 | result4.should.be.false(); 102 | store.all().length.should.eql(2); 103 | 104 | all[0].should.eql({ 105 | key:id1, 106 | value:{action:'input', idx:1, element:{type:'text', autocomplete:true}, tag:'input',length:24} 107 | }); 108 | 109 | all[1].should.eql({ 110 | key:id5, 111 | value:{action:'input', idx:5, element:{type:'text', autocomplete:true}, tag:'input',length:24} 112 | }); 113 | }); 114 | }); 115 | 116 | describe('.clear()', function(){ 117 | it('清空所有数据', function(){ 118 | for(var i = 0; i < 10; ++i){ 119 | store.add({action:'input', element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a1'); 120 | } 121 | store.clear(); 122 | var all = store.all(); 123 | all.length.should.eql(0); 124 | }); 125 | 126 | it('清空某种类型的数据', function(){ 127 | for(var i = 0; i <= 20; ++i){ 128 | var obj = {action:'input', element:{type:'text', autocomplete:true}, tag:'input',length:24, i:i}; 129 | var type = (i % 4 == 0) ? 'a1' :'a2'; 130 | store.add(obj, type); 131 | } 132 | store.clear('a1'); 133 | var all = store.all(); 134 | all.length.should.eql(15); 135 | }) 136 | }); 137 | 138 | describe('.all()', function(){ 139 | //确保每次hookrandom方法,测试完再重置 140 | beforeEach(function(){ 141 | Math.random = function(){ 142 | return .1234567;//1f9a1f9a-1f9a-1f9a-1f9a-1f9a1f9a1f9a 143 | } 144 | for(var i = 0; i < 10; ++i){ 145 | store.add({action:'input', element:{type:'text', autocomplete:true}, tag:'input',length:24}, 'a1'); 146 | } 147 | 148 | Math.random = function(){ 149 | return .7654321;//c3f3c3f3-c3f3-c3f3-c3f3-c3f3c3f3c3f3 150 | } 151 | for(i= 10; i < 20; ++i){ 152 | store.add({action:'click', element:{type:'checkbox', checked:true}, tag:'input'}, 'a2'); 153 | } 154 | }); 155 | 156 | it('返回一种类型的所有数据,如果没有指定类型,返回所有数据', function(){ 157 | var a1Data = store.all('a1'); 158 | a1Data.length.should.eql(10); 159 | a1Data.forEach(function(val, idx){ 160 | val.should.eql({ 161 | key:'1f9a1f9a-1f9a-1f9a-1f9a-1f9a1f9a1f9a', 162 | value:{action:'input', element:{type:'text', autocomplete:true}, tag:'input',length:24} 163 | }); 164 | }) 165 | 166 | var a2Data = store.all('a2'); 167 | a2Data.length.should.eql(10); 168 | a2Data.forEach(function(val, idx){ 169 | val.should.eql({ 170 | key:'c3f3c3f3-c3f3-c3f3-c3f3-c3f3c3f3c3f3', 171 | value:{action:'click', element:{type:'checkbox', checked:true}, tag:'input'} 172 | }); 173 | }); 174 | 175 | var all = store.all(); 176 | all.length.should.eql(20); 177 | 178 | }); 179 | 180 | it('如果指定类型没有数据,返回空数组', function(){ 181 | var empty = store.all('not-exist-type'); 182 | empty.length.should.eql(0); 183 | }); 184 | }); 185 | }) 186 | -------------------------------------------------------------------------------- /test/unit/trackerTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description tracker模块测试 4 | * 5 | */ 6 | var config = require('../../src/config'); 7 | var store = require('../../src/store'); 8 | var Userdata = require('../../src/userdata'); 9 | var Disptacher = require('../../src/dispatcher'); 10 | var Tracker = require('../../src/tracker'); 11 | var Action = require('../../src/action'); 12 | 13 | var actionMonitor = new Action(config, store); 14 | var userdata = new Userdata(); 15 | var dispatcher = new Disptacher(config); 16 | var tracker = new Tracker(config, store, dispatcher, userdata, actionMonitor); 17 | 18 | config.token = 'token-123-456-789'; 19 | 20 | describe('tracker module test', function(){ 21 | var newXhr,clock,report,sendError; 22 | beforeEach(function(){ 23 | newXhr = sinon.useFakeXMLHttpRequest(); 24 | clock = sinon.useFakeTimers(); 25 | report = sinon.spy(tracker, 'report');// 注意这里的用法sinon.spy(tracker.report)是找不到this而无法绑定正确的 26 | sendError = sinon.spy(dispatcher, 'sendError'); 27 | tracker.lastError = null; 28 | }); 29 | 30 | afterEach(function(){ 31 | sinon.restore(); 32 | clock.restore(); 33 | tracker.report.restore(); 34 | dispatcher.sendError.restore(); 35 | }); 36 | 37 | describe('.report()', function(done){ 38 | it('只发送允许间隔范围内的那些错误', function(){ 39 | for(var i = 0; i < 10; ++i){ 40 | var error = {message:'this is a test error', file:'trackerTest.js', line:300, column:223}; 41 | error.stack = '@debugger eval code 1:22'; 42 | tracker.catch(error, 'window'); 43 | clock.tick(200);//每200ms触发一次 44 | } 45 | report.callCount.should.eql(10); 46 | sendError.calledTwice.should.be.true(); 47 | }); 48 | }); 49 | 50 | describe('.catch()', function(){ 51 | it('捕获普通的错误信息', function(){ 52 | var error = {message:'this is a test error', file:'trackerTest.js', line:300, column:223}; 53 | error.stack = '@debugger eval code 1:22'; 54 | clock.tick(1457925878166);//2016-03-14T11:24:38+08:00 55 | tracker.catch(error, 'window'); 56 | report.calledOnce.should.be.true(); 57 | report.calledWithMatch({ 58 | version: config.settings.version, 59 | source: 'window', 60 | error: { 61 | message: 'this is a test error', 62 | file: 'trackerTest.js', 63 | line: 300, 64 | column: 223, 65 | stack: '@debugger eval code 1:22' 66 | }, 67 | url:location.href, 68 | time:'2016-03-14T11:24:38+08:00', 69 | token:'token-123-456-789', 70 | environment:{ 71 | vh:document.documentElement.clientHeight, 72 | vw:document.documentElement.clientWidth, 73 | ua:navigator.userAgent 74 | } 75 | }).should.be.true(); 76 | }); 77 | 78 | it('捕获错误,自动截断过长的堆栈信息', function(){ 79 | var error = {message:'this is a test error', file:'trackerTest.js', line:300, column:223}; 80 | config.settings.maxStackDepth = 5; 81 | var stackArr = ['@debugger eval code 1:22','a.js:1:12','b.js:2:3','c.js:3:4','d.js:4:5','e.js:5:6','f:js:6:7']; 82 | error.stack = stackArr.join('\n'); 83 | clock.tick(1457925878166);//2016-03-14T11:24:38+08:00 84 | tracker.catch(error, 'window'); 85 | report.calledOnce.should.be.true(); 86 | report.calledWithMatch({ 87 | version:config.settings.version, 88 | source:'window', 89 | error:{ 90 | message:'this is a test error', 91 | file:'trackerTest.js', 92 | line:300, 93 | column:223, 94 | stack:stackArr.slice(0,5).join('\n') 95 | }, 96 | url:location.href, 97 | time:'2016-03-14T11:24:38+08:00', 98 | token:'token-123-456-789', 99 | environment:{ 100 | vh:document.documentElement.clientHeight, 101 | vw:document.documentElement.clientWidth, 102 | ua:navigator.userAgent 103 | } 104 | }).should.be.true(); 105 | }); 106 | 107 | it('默认的config配置发送operation和userdata', function(){ 108 | userdata.set({ 109 | name:'liuzhen7', 110 | age:30 111 | }); 112 | 113 | var error = {message:'this is a test error', file:'trackerTest.js', line:300, column:223}; 114 | error.stack = '@debugger eval code 1:22'; 115 | clock.tick(1457925878166);//2016-03-14T11:24:38+08:00 116 | tracker.catch(error, 'window'); 117 | 118 | sendError.calledOnce.should.be.true(); 119 | sendError.calledWithMatch({ 120 | version: config.settings.version, 121 | source:'window', 122 | error:{ 123 | message:'this is a test error', 124 | file:'trackerTest.js', 125 | line:300, 126 | column:223, 127 | stack:'@debugger eval code 1:22' 128 | }, 129 | operation:[], 130 | userdata:{ 131 | name:'liuzhen7', 132 | age:30 133 | }, 134 | url:location.href, 135 | time:'2016-03-14T11:24:38+08:00', 136 | token:'token-123-456-789', 137 | environment:{ 138 | vh:document.documentElement.clientHeight, 139 | vw:document.documentElement.clientWidth, 140 | ua:navigator.userAgent 141 | } 142 | }).should.be.true(); 143 | }); 144 | 145 | it('通过config配置不发送operation', function(){ 146 | userdata.set({ 147 | name:'liuzhen7', 148 | age:30 149 | }); 150 | config.action = false 151 | 152 | var error = {message:'this is a test error', file:'trackerTest.js', line:300, column:223}; 153 | error.stack = '@debugger eval code 1:22'; 154 | clock.tick(1457925878166);//2016-03-14T11:24:38+08:00 155 | tracker.catch(error, 'window'); 156 | 157 | sendError.calledOnce.should.be.true(); 158 | sendError.calledWithMatch({ 159 | version: config.settings.version, 160 | source:'window', 161 | error:{ 162 | message:'this is a test error', 163 | file:'trackerTest.js', 164 | line:300, 165 | column:223, 166 | stack:'@debugger eval code 1:22' 167 | }, 168 | userdata:{ 169 | name:'liuzhen7', 170 | age:30 171 | }, 172 | url:location.href, 173 | time:'2016-03-14T11:24:38+08:00', 174 | token:'token-123-456-789', 175 | environment:{ 176 | vh:document.documentElement.clientHeight, 177 | vw:document.documentElement.clientWidth, 178 | ua:navigator.userAgent 179 | } 180 | }).should.be.true(); 181 | }) 182 | }); 183 | 184 | }); 185 | -------------------------------------------------------------------------------- /test/unit/userdataTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description userdata测试模块 4 | * 5 | */ 6 | 7 | var ud = require('../../src/userdata'); 8 | var userdata = new ud(); 9 | 10 | describe('userdata module test', function(){ 11 | beforeEach(function(){ 12 | userdata.set('name','liuzhen7'); 13 | userdata.set('age',30); 14 | userdata.set('extra',{p1:1,p2:2}); 15 | userdata.set({ 16 | homework:'read a book', 17 | done:true 18 | }); 19 | }); 20 | 21 | afterEach(function(){ 22 | userdata.clear(); 23 | }); 24 | 25 | describe('.get() and .set()', function(){ 26 | it('设置和获取用户自定义信息', function(){ 27 | 28 | userdata.set('extra',{p1:2,p2:3,p3:4}); 29 | 30 | var v1 = userdata.get('age'); 31 | var v2 = userdata.get('done'); 32 | v1.should.eql(30); 33 | v2.should.be.true(); 34 | 35 | userdata.get().should.eql({ 36 | name:'liuzhen7', 37 | age:30, 38 | extra:{p1:2,p2:3,p3:4}, 39 | homework:'read a book', 40 | done:true 41 | }); 42 | }); 43 | }); 44 | 45 | describe('.remove()', function(){ 46 | it('给定一个key,删除对应的项', function(){ 47 | userdata.remove('age'); 48 | userdata.remove('extra'); 49 | userdata.get().should.eql({ 50 | name:'liuzhen7', 51 | homework:'read a book', 52 | done:true 53 | }) 54 | }); 55 | }); 56 | 57 | describe('.clear()', function(){ 58 | it('清空所有的项', function(){ 59 | userdata.clear(); 60 | userdata.get().should.eql({}); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/unit/utilTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description util模块,有一些函数没有办法测试,isoDate\guid 4 | * 5 | */ 6 | var util = require('../../src/util'); 7 | 8 | describe('util module test',function(){ 9 | describe('object real type judgement', function(){ 10 | var a = {}; 11 | var b = null; 12 | var c = {a:1,b:2}; 13 | var d = true; 14 | var e = [1,2,3,4,5]; 15 | var f = 'I\'m a boy'; 16 | var g = 12345; 17 | var h = undefined; 18 | var i = function(){ 19 | //nop function 20 | } 21 | var j = /\w{1,10}/ig; 22 | var k = new Date; 23 | var l = new EvalError; 24 | it('.isPlainObject()', function(){ 25 | util.isPlainObject(a).should.be.true(); 26 | util.isPlainObject(b).should.be.false(); 27 | util.isPlainObject(c).should.be.true(); 28 | util.isPlainObject(d).should.be.false(); 29 | util.isPlainObject(e).should.be.false(); 30 | util.isPlainObject(f).should.be.false(); 31 | util.isPlainObject(g).should.be.false(); 32 | util.isPlainObject(h).should.be.false(); 33 | util.isPlainObject(i).should.be.false(); 34 | util.isPlainObject(j).should.be.false(); 35 | util.isPlainObject(k).should.be.false(); 36 | util.isPlainObject(l).should.be.false(); 37 | }); 38 | it('.isArray()', function(){ 39 | util.isArray(a).should.be.false(); 40 | util.isArray(b).should.be.false(); 41 | util.isArray(c).should.be.false(); 42 | util.isArray(d).should.be.false(); 43 | util.isArray(e).should.be.true(); 44 | util.isArray(f).should.be.false(); 45 | util.isArray(g).should.be.false(); 46 | util.isArray(h).should.be.false(); 47 | util.isArray(i).should.be.false(); 48 | util.isArray(j).should.be.false(); 49 | util.isArray(k).should.be.false(); 50 | util.isArray(l).should.be.false(); 51 | }); 52 | it('.isBoolean()', function(){ 53 | util.isBoolean(a).should.be.false(); 54 | util.isBoolean(b).should.be.false(); 55 | util.isBoolean(c).should.be.false(); 56 | util.isBoolean(d).should.be.true(); 57 | util.isBoolean(e).should.be.false(); 58 | util.isBoolean(f).should.be.false(); 59 | util.isBoolean(g).should.be.false(); 60 | util.isBoolean(h).should.be.false(); 61 | util.isBoolean(i).should.be.false(); 62 | util.isBoolean(j).should.be.false(); 63 | util.isBoolean(k).should.be.false(); 64 | util.isBoolean(l).should.be.false(); 65 | }); 66 | it('.isDate()', function(){ 67 | util.isDate(a).should.be.false(); 68 | util.isDate(b).should.be.false(); 69 | util.isDate(c).should.be.false(); 70 | util.isDate(d).should.be.false(); 71 | util.isDate(e).should.be.false(); 72 | util.isDate(f).should.be.false(); 73 | util.isDate(g).should.be.false(); 74 | util.isDate(h).should.be.false(); 75 | util.isDate(i).should.be.false(); 76 | util.isDate(j).should.be.false(); 77 | util.isDate(k).should.be.true(); 78 | util.isDate(l).should.be.false(); 79 | }); 80 | it('.isNumber()', function(){ 81 | util.isNumber(a).should.be.false(); 82 | util.isNumber(b).should.be.false(); 83 | util.isNumber(c).should.be.false(); 84 | util.isNumber(d).should.be.false(); 85 | util.isNumber(e).should.be.false(); 86 | util.isNumber(f).should.be.false(); 87 | util.isNumber(g).should.be.true(); 88 | util.isNumber(h).should.be.false(); 89 | util.isNumber(i).should.be.false(); 90 | util.isNumber(j).should.be.false(); 91 | util.isNumber(k).should.be.false(); 92 | util.isNumber(l).should.be.false(); 93 | }); 94 | it('.isObject()', function(){ 95 | util.isObject(a).should.be.true(); 96 | util.isObject(b).should.be.false(); 97 | util.isObject(c).should.be.true(); 98 | util.isObject(d).should.be.false(); 99 | util.isObject(e).should.be.false(); 100 | util.isObject(f).should.be.false(); 101 | util.isObject(g).should.be.false(); 102 | util.isObject(h).should.be.false(); 103 | util.isObject(i).should.be.false(); 104 | util.isObject(j).should.be.false(); 105 | util.isObject(k).should.be.false(); 106 | util.isObject(l).should.be.false(); 107 | }); 108 | it('.isRegExp()', function(){ 109 | util.isRegExp(a).should.be.false(); 110 | util.isRegExp(b).should.be.false(); 111 | util.isRegExp(c).should.be.false(); 112 | util.isRegExp(d).should.be.false(); 113 | util.isRegExp(e).should.be.false(); 114 | util.isRegExp(f).should.be.false(); 115 | util.isRegExp(g).should.be.false(); 116 | util.isRegExp(h).should.be.false(); 117 | util.isRegExp(i).should.be.false(); 118 | util.isRegExp(j).should.be.true(); 119 | util.isRegExp(k).should.be.false(); 120 | util.isRegExp(l).should.be.false(); 121 | }); 122 | it('.isString()', function(){ 123 | util.isString(a).should.be.false(); 124 | util.isString(b).should.be.false(); 125 | util.isString(c).should.be.false(); 126 | util.isString(d).should.be.false(); 127 | util.isString(e).should.be.false(); 128 | util.isString(f).should.be.true(); 129 | util.isString(g).should.be.false(); 130 | util.isString(h).should.be.false(); 131 | util.isString(i).should.be.false(); 132 | util.isString(j).should.be.false(); 133 | util.isString(k).should.be.false(); 134 | util.isString(l).should.be.false(); 135 | }); 136 | 137 | it('.isError()', function(){ 138 | util.isError(a).should.be.false(); 139 | util.isError(b).should.be.false(); 140 | util.isError(c).should.be.false(); 141 | util.isError(d).should.be.false(); 142 | util.isError(e).should.be.false(); 143 | util.isError(f).should.be.false(); 144 | util.isError(g).should.be.false(); 145 | util.isError(h).should.be.false(); 146 | util.isError(i).should.be.false(); 147 | util.isError(j).should.be.false(); 148 | util.isError(k).should.be.false(); 149 | util.isError(l).should.be.true(); 150 | }); 151 | }) 152 | 153 | describe('.bind()', function(){ 154 | it('简单bind this', function(){ 155 | var getName = function(){ 156 | return 'hi '+ this.name; 157 | } 158 | 159 | var wrappedFunc = util.bind(getName, {name:'liuzhen'}); 160 | var val = wrappedFunc(); 161 | val.should.eql('hi liuzhen'); 162 | }) 163 | 164 | it('绑定除this以外额外的参数', function(){ 165 | var addNum = function(num1, num2, num3){ 166 | return num1 + num2 + num3; 167 | } 168 | 169 | var spy = sinon.spy(addNum); 170 | var wrappedFunc = util.bind(spy, null, 2, 3); 171 | var result = wrappedFunc(4); 172 | result.should.eql(9); 173 | 174 | spy.called.should.be.true(); 175 | spy.calledWithExactly(2,3,4).should.be.true(); 176 | }); 177 | }); 178 | 179 | describe('.isIE()', function(){ 180 | it('检测是否ie浏览器', function(){ 181 | var ua = navigator.userAgent.toLowerCase(); 182 | util.isIE().should.equal(ua.indexOf('msie') > -1 || ua.indexOf('trident') > -1) 183 | }); 184 | }); 185 | 186 | describe('.pad()', function(){ 187 | var num = 123; 188 | 189 | it('如果数值长度小于要pad的长度,不会pad', function(){ 190 | var r1 = util.pad(num, 1); 191 | var r2 = util.pad(num, 2); 192 | r1.should.eql(num); 193 | r2.should.eql(num); 194 | }) 195 | 196 | it('如果数值长度大于要pad的长度,在左侧pad相应的0', function(){ 197 | var r1 = util.pad(num, 5); 198 | var r2 = util.pad(num, 10); 199 | 200 | r1.should.eql('00123'); 201 | r2.should.eql('0000000123'); 202 | }); 203 | }); 204 | 205 | describe('.isoDate()', function(){ 206 | var clock; 207 | 208 | beforeEach(function(){ 209 | clock = sinon.useFakeTimers(); 210 | }); 211 | 212 | afterEach(function(){ 213 | clock.restore(); 214 | }); 215 | 216 | it('返回一个iso8601格式的utc时间,并且带相应的timezone信息', function(){ 217 | clock.tick(1457510544000)//2016-03-09T08:02:24Z 218 | util.isoDate().should.eql('2016-03-09T16:02:24+08:00'); 219 | }); 220 | }); 221 | 222 | describe.skip('.isOldIE() .isIE67() .cw() .guid() .addEvent() .removeEvent() .indexOf() .globalObjValue()', function(){}); 223 | }); 224 | -------------------------------------------------------------------------------- /test/unit/windowTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @description window模块测试 4 | * 5 | */ 6 | var config = require('../../src/config'); 7 | var store = require('../../src/store'); 8 | var Dispatcher = require('../../src/dispatcher'); 9 | var Tracker = require('../../src/tracker'); 10 | 11 | var tracker = new Tracker(config, store, new Dispatcher(config)); 12 | var win = require('../../src/window'); 13 | 14 | 15 | describe.skip('window module test', function(){ 16 | describe('catch error 没想到好的办法测试', function(){ 17 | var globalError,catchError; 18 | beforeEach(function(){ 19 | win.monitor(config, tracker); 20 | globalError = sinon.spy(window, 'onerror'); 21 | catchError = sinon.stub(tracker, 'catch', function(err, type){ 22 | console.log(err, type); 23 | }); 24 | }); 25 | 26 | afterEach(function(){ 27 | window.onerror.restore(); 28 | tracker.catch.restore(); 29 | }); 30 | 31 | it('全局捕获错误', function(){ 32 | var callback = sinon.stub(); 33 | // callback.withArgs(1).throws("TypeError"); 34 | // callback(1); 35 | // globalError.called.should.be.true(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var merge = require('webpack-merge') 4 | 5 | var env = process.env.NODE_ENV 6 | var base = { 7 | entry: { 8 | tracker: './src/index.js' 9 | }, 10 | output: { 11 | path: './dist', 12 | filename: '[name].js' 13 | }, 14 | resolve: { 15 | extensions: ['', '.js'], 16 | fallback: [path.join(__dirname, '../node_modules')], 17 | }, 18 | plugins: [ 19 | new webpack.DefinePlugin({ 20 | 'process.env': '\''+ env + '\'' 21 | }), 22 | ] 23 | } 24 | 25 | switch (env) { 26 | case 'production': 27 | module.exports = merge(base, { 28 | output: { 29 | filename: '[name].min.js' 30 | }, 31 | devtool: '#source-map', 32 | plugins: [ 33 | new webpack.optimize.UglifyJsPlugin({ 34 | compress: { 35 | warnings: false 36 | } 37 | }), 38 | ] 39 | }) 40 | break 41 | case 'test': 42 | module.exports = merge(base, { 43 | devtool: '#inline-source-map', 44 | module: { 45 | preLoaders: [{ 46 | test: /\.js$/, 47 | loader: 'isparta', 48 | include: path.resolve(__dirname, 'src') 49 | }] 50 | }, 51 | }) 52 | break 53 | case 'development' : 54 | module.exports = base 55 | break 56 | } 57 | --------------------------------------------------------------------------------