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