├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── build.js └── config.js ├── dist ├── vue-dataAc.common.js ├── vue-dataAc.esm.browser.js ├── vue-dataAc.esm.browser.min.js ├── vue-dataAc.esm.js ├── vue-dataAc.js └── vue-dataAc.min.js ├── example ├── app.js ├── appoint │ ├── app.js │ └── index.html ├── basic │ ├── app.js │ └── index.html ├── error │ ├── app.js │ └── index.html ├── imgreport │ ├── app.js │ └── index.html ├── index.html ├── lib │ ├── axios.min.js │ ├── image │ │ └── ac.png │ ├── iview.min.js │ ├── menu.js │ ├── styles │ │ ├── basic.css │ │ ├── fonts │ │ │ ├── ionicons.svg │ │ │ ├── ionicons.ttf │ │ │ ├── ionicons.woff │ │ │ └── ionicons.woff2 │ │ └── iview.css │ ├── vue-dataAc.js │ ├── vue-dataAc.min.js │ ├── vue-router.min.js │ └── vue.min.js ├── log │ ├── app.js │ └── index.html ├── performance │ ├── app.js │ └── index.html ├── reportsize │ ├── app.js │ └── index.html └── token │ ├── app.js │ └── index.html ├── package-lock.json ├── package.json ├── spec └── support │ └── jasmine.json └── src ├── config └── config.js ├── index.js ├── install.js └── util └── util.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | globals: { 10 | ActiveXObject: true, 11 | XMLHttpRequest: true, 12 | window: true, 13 | Image: true, 14 | document: true, 15 | }, 16 | rules: { 17 | // 0 禁用此规则 1 不符合规则即给出警告 2 不符合规则即报错 18 | 'accessor-pairs': 2,// 在对象中使用getter/setter 19 | 'arrow-spacing': [2, { 'before': true, 'after': true }],// 箭头函数前后括号 20 | 'brace-style': [2, '1tbs', { 'allowSingleLine': true }],// 大括号风格,允许写在一行 https://eslint.org/docs/rules/brace-style#require-brace-style-brace-style 21 | 'comma-dangle': [2, 'never'],// 对象字面量项尾不能有逗号 22 | 'constructor-super': 2,// 非派生类不能调用super,派生类必须调用super 23 | 'curly': [2, 'multi-line'],// 块级作用域可以不带大括号 https://eslint.org/docs/rules/curly#require-following-curly-brace-conventions-curly 24 | 'dot-location': [2, 'property'],// 对象访问符的位置,换行的时候在行首 https://eslint.org/docs/rules/dot-location#enforce-newline-before-and-after-dot-dot-location 25 | 'eqeqeq': [2, 'allow-null'], // 必须使用全等 26 | 'handle-callback-err': [2, '^(err|error)$' ],// nodejs函数处理错误 27 | 'new-cap': [2, { 'newIsCap': true, 'capIsNew': false }],// 新建对象实例首字母必须大写 28 | 'new-parens': 2,// new时必须加小括号 29 | 'no-array-constructor': 2,// 禁止使用数组构造器 https://eslint.org/docs/rules/no-array-constructor#rule-details 30 | 'no-class-assign': 2, // 禁止给类赋值 31 | 'no-cond-assign': 2,// 禁止在条件表达式中使用赋值语句 32 | 'no-const-assign': 2,//禁止修改const声明的变量 33 | 'no-control-regex': 2,//禁止在正则表达式中使用控制字符 34 | 'no-delete-var': 2,//不能对var声明的变量使用delete操作符 35 | 'no-dupe-args': 2,//函数参数不能重复 36 | 'no-dupe-class-members': 2, //对象成员不能重复 37 | 'no-dupe-keys': 2,//在创建对象字面量时不允许键重复 38 | 'no-duplicate-case': 2,//switch中的case标签不能重复 39 | 'no-empty-character-class': 2,//正则表达式中的[]内容不能为空 40 | 'no-empty-pattern': 2,// https://eslint.org/docs/rules/no-empty-pattern#version 41 | 'no-eval': 2,//禁止使用eval 42 | 'no-ex-assign': 2,//禁止给catch语句中的异常参数赋值 43 | 'no-extend-native': 2,//禁止扩展native对象 44 | 'no-extra-bind': 2,//禁止不必要的函数绑定 45 | 'no-extra-boolean-cast': 2,//禁止不必要的bool转换 46 | 'no-extra-parens': [2, 'functions'],//禁止非必要的括号 47 | 'no-fallthrough': 2,//禁止switch穿透 48 | 'no-floating-decimal': 2,//禁止省略浮点数中的0 .5 3. 49 | 'no-func-assign': 2,//禁止重复的函数声明 50 | 'no-implied-eval': 2,////禁止使用隐式eval 51 | 'no-inner-declarations': [2, 'functions'],//禁止在块语句中使用声明(变量或函数) 52 | 'no-invalid-regexp': 2,//禁止无效的正则表达式 53 | 'no-iterator': 2,//禁止使用__iterator__ 属性 54 | 'no-label-var': 2,//label名不能与var声明的变量名相同 55 | 'no-labels': [2, { 'allowLoop': false, 'allowSwitch': false }], 56 | 'no-lone-blocks': 2,//禁止标签声明 57 | 'no-multi-str': 2,//字符串不能用\换行 58 | 'no-multiple-empty-lines': [2, { 'max': 1 }],//空行最多不能超过2行 59 | 'no-native-reassign': 2,//不能重写native对象 60 | 'no-negated-in-lhs': 2,//in 操作符的左边不能有! 61 | 'no-new-object': 2,//禁止使用new Object() 62 | 'no-new-require': 2,//禁止使用new require 63 | 'no-new-symbol': 2,// 使用Symbol()而不能使用new 64 | 'no-new-wrappers': 2,// https://eslint.org/docs/rules/no-new-wrappers#disallow-primitive-wrapper-instances-no-new-wrappers 65 | 'no-obj-calls': 2,//不能调用内置的全局对象,比如Math() JSON() 66 | 'no-octal': 2,//禁止使用八进制数字 67 | 'no-octal-escape': 2,//禁止使用八进制转义序列 68 | 'no-path-concat': 2,//node中不能使用__dirname或__filename做路径拼接 69 | 'no-proto': 2,//禁止使用__proto__属性 70 | 'no-redeclare': 2,//禁止重复声明变量 71 | 'no-return-assign': [2, 'except-parens'],//return 语句中不能有赋值表达式 72 | 'no-self-assign': 2,// 不能自声明 73 | 'no-self-compare': 2,// 不能自比较 74 | 'no-sequences': 2,//禁止使用逗号运算符 75 | 'no-shadow-restricted-names': 2,//严格模式中规定的限制标识符不能作为声明时的变量名使用 76 | 'no-sparse-arrays': 2,//禁止稀疏数组, [1,,2] 77 | 'no-this-before-super': 2,//在调用super()之前不能使用this或super 78 | 'no-throw-literal': 2,//禁止抛出字面量错误 throw "error"; 79 | 'no-undef': 2,//不能有未定义的变量 80 | 'no-undef-init': 2,//变量初始化时不能直接给它赋值为undefined 81 | 'no-unexpected-multiline': 2,//避免多行表达式 82 | 'no-unmodified-loop-condition': 2,//不使用未定义的循环条件 83 | 'no-unneeded-ternary': [2, { 'defaultAssignment': false }],//禁止不必要的嵌套 https://eslint.org/docs/rules/no-unneeded-ternary#disallow-ternary-operators-when-simpler-alternatives-exist-no-unneeded-ternary 84 | 'no-unreachable': 2,//不能有无法执行的代码 85 | 'no-unsafe-finally': 2,// finally中不能执行有歧义的代码 86 | 'no-unused-vars': [2, { 'vars': 'all', 'args': 'none' }],//不声明未使用的变量 87 | 'no-useless-call': 2,//禁止不必要的call和apply 88 | 'no-useless-computed-key': 2,//不声明无用的键 89 | 'no-useless-constructor': 2,// https://eslint.org/docs/rules/no-useless-constructor#disallow-unnecessary-constructor-no-useless-constructor 90 | 'no-useless-escape': 0,// https://eslint.org/docs/rules/no-useless-escape#disallow-unnecessary-escape-usage-no-useless-escape 91 | 'no-with': 2,//禁用with 92 | 'one-var': [2, { 'initialized': 'never' }],//禁用连续声明 93 | 'operator-linebreak': [2, 'after', { 'overrides': { '?': 'before', ':': 'before' } }],//换行时运算符在行尾还是行首 94 | 'padded-blocks': [2, 'never'],//块语句内行首行尾不能空行 95 | 'use-isnan': 2,//禁止比较时使用NaN,只能用isNaN() 96 | 'valid-typeof': 2,//必须使用合法的typeof的值 97 | 'wrap-iife': [2, 'any'],//立即执行函数表达式的小括号风格任意一种都可以 98 | 'yield-star-spacing': [2, 'both'],// generate 函数 yeild风格 99 | 'yoda': [2, 'never'],//禁止尤达条件 100 | 'prefer-const': 2,//优先使用const 101 | } 102 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | TODOs.md 4 | .idea 5 | .vscode/settings.json 6 | .env 7 | selenium-server.log 8 | local.log 9 | browserstack.err -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue-dataAc - Vue 数据采集上报插件 2 | 3 | ## 写在前面 4 | 5 | - 后端项目参考: [Vue-dataAc-server](https://github.com/Cc-Edit/Vue-dataAc-server) 6 | - 此插件基于 [dataAcquisition](https://github.com/Cc-Edit/dataAcquisition) 进行重构 7 | - 基于Vue进行插件开发,新增了很多配置,也对整体的采集监控做了优化,让这一切更优雅更灵活更简单。 8 | - 项目初期,难免有一些不同场景下的问题,大家在使用过程中遇到任何问题,或者有不满意的点都可以提交issue上来。 9 | - 另外: https://data.ccedit.com/logStash/push 作为测试接口使用,会不定期清空数据,请不要上报真实数据 10 | 11 | | 学习讨论小组🍻 | 打赏(赠送学习资料:[webNote](https://github.com/Cc-Edit/webNote)) :confetti_ball: | 12 | |:----------------------------------------------------------------------------------:|:---------------------------------------------------------------------:| 13 | | ![wechat.png](https://github.com/Cc-Edit/Cc-Edit/blob/main/public/CcClip.png) | ![img.png](https://github.com/Cc-Edit/Cc-Edit/blob/main/public/img.png) | 14 | 15 | 16 | ## 快速开始 17 | 18 | **安装** 19 | ``` 20 | npm install vue-dataac --save 21 | ``` 22 | 23 | **Vue Cli** 24 | ``` 25 | import Vue from 'vue' 26 | import VueDataAc from 'vue-dataac' 27 | 28 | Vue.use(VueDataAc, { 29 | // imageUrl: 'https://data.ccedit.com/lib/image/ac.png' 30 | // or 31 | useImgSend: false, 32 | postUrl: 'https://data.ccedit.com/logStash/push' 33 | }); 34 | ``` 35 | 36 | **ES6** 37 | ``` 38 | import VueDataAc from 'vue-dataac' 39 | ``` 40 | 41 | **CommonJS** 42 | ``` 43 | var VueDataAc = require('vue-dataac'); 44 | ``` 45 | 46 | **直接引用** 47 | ``` 48 | 49 | ``` 50 | 51 | 52 | ## demo: 53 | | 功能 | demo地址 | 数据分析展示 | 54 | | :------------ |:------------------------------------------------|:----------------------------------------| 55 | | 文档 | 'https://data.ccedit.com/index.html' | '' | 56 | | 行为监控Demo | 'https://data.ccedit.com/basic/index.html' | 'https://data.ccedit.com/log/index.html' | 57 | | 异常监控Demo | 'https://data.ccedit.com/error/index.html' | 'https://data.ccedit.com/log/index.html' | 58 | | 性能监控Demo | 'https://data.ccedit.com/performance/index.html' | 'https://data.ccedit.com/log/index.html' | 59 | | 主动埋点Demo | 'https://data.ccedit.com/appoint/index.html' | 'https://data.ccedit.com/log/index.html' | 60 | | 图片数据上报Demo | 'https://data.ccedit.com/imgreport/index.html' | 'https://data.ccedit.com/log/index.html' | 61 | | 上报节流Demo(sizeLimit) | 'https://data.ccedit.com/reportsize/index.html' | 'https://data.ccedit.com/log/index.html' | 62 | | 关联登录信息Demo | 'https://data.ccedit.com/token/index.html' | 'https://data.ccedit.com/log/index.html' | 63 | 64 | 65 | ## 文档: 66 | 为了尽可能灵活,以下所有配置项理论上都可以修改配置, 67 | 我对每个配置项做了修改建议,供大家参考: 68 | :smile: = 可以修改 69 | :neutral_face: = 最好不修改 70 | :rage: = 千万不要修改 71 | 72 | ### 1. 标识类配置,作为数据上报信息的分类标识 73 | | 配置项 | 类型 | 默认值 | 是否可配置 | 说明 | 生效版本 | 74 | | :------------ |:---------------| :---------------|:---------------|:---------------|:-----:| 75 | | storeInput | String | 'ACINPUT' | :neutral_face: | 输入框行为采集标识 | 1.0.0 | 76 | | storePage | String | 'ACPAGE' | :neutral_face: | 页面访问信息采集标识 |1.0.0 | 77 | | storeClick | String | 'ACCLIK' | :neutral_face: | 点击事件采集标识 |1.0.0 | 78 | | storeReqErr | String | 'ACRERR' | :neutral_face: | 请求异常采集标识 |1.0.0 | 79 | | storeTiming | String | 'ACTIME' | :neutral_face: | 页面性能采集标识 |1.0.0 | 80 | | storeCodeErr | String | 'ACCERR' | :neutral_face: | 代码异常采集标识 |1.0.0 | 81 | | storeCustom | String | 'ACCUSTOM' | :neutral_face: | 自定义事件采集标识 | 2.0.0 | 82 | | storeSourceErr | String | 'ACSCERR' | :neutral_face: | 资源加载异常采集标识 |2.0.0 | 83 | | storePrmseErr | String | 'ACPRERR' | :neutral_face: | promise抛出异常标识 |2.0.0 | 84 | | storeCompErr | String | 'ACCOMP' | :neutral_face: | Vue组件性能监控标识 |2.0.0 | 85 | | storeVueErr | String | 'ACVUERR' | :neutral_face: | Vue异常监控标识 |2.0.0 | 86 | 87 | ### 2. 全局开关,用来自定义采集范围 88 | | 配置项 | 类型 | 默认值 | 是否可配置 | 说明 | 生效版本 | 89 | | :------------ |:---------------| :---------------|:---------------|:---------------|:-----:| 90 | | userSha | String | 'vue_ac_userSha' | :smile: | 用户标识存储在Storage中的key,有冲突可修改 | 1.0.0 | 91 | | useImgSend | Boolean | true | :smile: | 是否使用图片上报数据, 设置为 false 为 xhr 接口请求上报 | 2.0.0 | 92 | | useStorage | Boolean | true | :smile: | 是否使用storage作为存储载体, 设置为 false 时使用cookie | 2.0.0 | 93 | | maxDays | Number | 365 | :smile: | 如果使用cookie作为存储载体,此项生效,配置cookie存储时间,默认一年 | 2.0.0 | 94 | | openInput | Boolean | true | :smile: | 是否开启输入数据采集 | 1.0.0 | 95 | | openCodeErr | Boolean | true | :smile: | 是否开启代码异常采集 | 1.0.0 | 96 | | openClick | Boolean | true | :smile: | 是否开启点击数据采集 | 1.0.0 | 97 | | openXhrQuery | Boolean | true | :smile: | 采集接口异常时是否采集请求参数params | 2.0.0 | 98 | | openXhrHock | Boolean | true | :smile: | 是否开启xhr异常采集 | 1.0.0 | 99 | | openPerformance | Boolean | true | :smile: | 是否开启页面性能采集 | 1.0.0 | 100 | | openPage | Boolean | true | :smile: | 是否开启页面访问信息采集(PV/UV) | 2.0.0 | 101 | | openVueErr | Boolean | true | :smile: | 是否开启Vue异常监控 | 2.0.0 | 102 | | openSourceErr | Boolean | true | :smile: | 是否开启资源加载异常采集 | 2.0.0 | 103 | | openPromiseErr | Boolean | true | :smile: | 是否开启promise异常采集 | 2.0.0 | 104 | | openComponent | Boolean | true | :smile: | 是否开启组件性能采集 | 2.0.0 | 105 | | maxComponentLoadTime | Number | 1000 | :rage: | 组件渲染超时阈值,太小会导致信息过多,出发点是找出渲染异常的组件 | 2.0.0 | 106 | | openXhrTimeOut | Boolean | true | :smile: | 是否开启请求超时上报 | 2.0.0 | 107 | | maxRequestTime | Number | 10000 | :smile: | 请求时间阈值,请求到响应大于此时间,会上报异常,openXhrTimeOut 为 false 时不生效 | 2.0.0 | 108 | | customXhrErrCode | String | '' | :smile: | 支持自定义响应code,当接口响应中的code为指定内容时上报异常 | 2.0.0 | 109 | 110 | ### 3. 行为采集配置 111 | | 配置项 | 类型 | 默认值 | 采集范围 | 是否可配置 | 说明 | 生效版本 | 112 | | :------------ |:---------------| :---------------| :---------------|:---------------|:---------------|:-----:| 113 | | selector | String | 'input' | 所有input输入框(全量采集) | :smile: | 通过控制选择器来限定监听范围,使用document.querySelectorAll进行选择,值参考:https://www.runoob.com/cssref/css-selectors.html | 1.0.0 | 114 | | selector | String | 'input.isjs-ac' | 所有class包含isjs-ac的input输入框(埋点采集) | :smile: | 通过控制选择器来限定监听范围,使用document.querySelectorAll进行选择,值参考:https://www.runoob.com/cssref/css-selectors.html | 1.0.0 | 115 | | ignoreInputType | Array | `['password', 'file']` | type不是password和file的输入框 | :smile: | --- | 1.0.0 | 116 | | ignoreInputType | Array | `[]` | 所有输入框 | :smile: | --- | 2.0.0 | 117 | | classTag | String | '' | 所有可点击元素(全量采集) | :smile: | 点击事件埋点标识, 自动埋点时请配置空字符串| 1.0.0 | 118 | | classTag | String | 'isjs-ac' | 只会采集 class 包含 isjs-ac 元素的点击(埋点采集) | :smile: | 点击事件埋点标识, 自动埋点时请配置空字符串| 1.0.0 | 119 | | maxHelpfulCount | Number | 5 | 全量采集和埋点采集场景下,为了使上报数据准确,我们会递归父元素,找到一个有class或id的祖先元素,此项配置递归次数 | :neutral_face: | 页面层次较深情况下,建议保留配置,以减少性能损耗 | 2.0.0 | 120 | 121 | ### 4. 数据上报配置 122 | | 配置项 | 类型 | 默认值 | 是否可配置 | 说明 | 生效版本 | 123 | | :------------ |:---------------|:------------------------------------------| :---------------|:---------------|:---------------| 124 | | imageUrl | String | 'https://data.ccedit.com/lib/image/ac.png' | :smile: | 《建议》 图片上报地址(通过1*1px图片接收上报信息)依赖 useImgSend 配置打开| 1.0.0 | 125 | | postUrl | String | 'https://data.ccedit.com/logStash/push' | :smile: | 接口上报地址 | 1.0.0 | 126 | | openReducer | Boolean | false | :smile: | 是否开启节流,用于限制上报频率,开启后sizeLimit,manualReport生效 | 2.0.0 | 127 | | sizeLimit | Number | 20 | :smile: | 采集数据超过指定条目时自动上报,依赖 openReducer == true, 优先级:2 | 2.0.0 | 128 | | cacheEventStorage | String | 'ac_cache_data' | :smile: | 开启节流后数据本地存储key | 2.0.0 | 129 | | manualReport | Boolean | false | :smile: | 强制手动上报,开启后只能调用postAcData方法上报,依赖 openReducer == true,优先级:1 | 2.0.0 | 130 | 131 | ### 5. 实例方法 132 | 133 | #### 1. vue.$vueDataAc.setCustomAc( {cusKey: String, cusVal: Any} ) 134 | 用于自定义事件的约定上报,例如在业务场景中对某些逻辑的埋点。 135 | 自定义事件上报逻辑与其他事件上报共用,可以通过openReducer限制频率 136 | 137 | #### 2. vue.$vueDataAc.postAcData() 138 | 手动上报当前采集信息 139 | 140 | #### 3. vue.$vueDataAc.setUserToken(userToken: String) 141 | 用于关联用户后台标记,利用用户登录后的userid,sessionId 142 | 目的是将前后台日志打通,方便查找模拟用户 143 | 144 | 145 | ## 上报数据格式: 146 | 147 | ### 1. 页面访问,路由跳转,等同于PV/UV数据: 148 | 149 | ``` 150 | { 151 | "uuid": "F6A6C801B7197603", //用户标识 152 | "t" : "", //后端 用户标识/登录标识 默认为空,通过setUserToken设置 153 | "acData" : { 154 | "type" : "ACPAGE" //行为标识 155 | "sTme" : 1591760011268 //数据上报时间 156 | "fromPath" : "/register?type=1" //来源路由 157 | "formParams" : "{'type': 1}" //来源参数 158 | "toPath" : "/login" //目标路由 159 | "toParams" : "{}" //目标参数 160 | "inTime" : 1591760011268 //页面进入时间 161 | "outTime" : 1591760073422 //离开页面时间 162 | } 163 | } 164 | ``` 165 | 166 | ### 2. 代码异常数据 167 | 168 | ``` 169 | { 170 | "uuid": "F6A6C801B7197603", //用户标识 171 | "t" : "", 172 | "acData" : { 173 | "type" : "ACCERR", //上报数据类型:代码异常 174 | "path" : "www.domain.com/w/w/w/", //事件发生页面的url 175 | "sTme" : "1591760073422", //事件上报时间 176 | "msg" : "script error", //异常摘要 177 | "line" : "301", //代码行数 178 | "col" : "13", //代码列下标 179 | "err" : "error message", //错误信息 180 | } 181 | } 182 | ``` 183 | 184 | ### 3. 资源加载异常数据 185 | 186 | ``` 187 | { 188 | "uuid": "F6A6C801B7197603", //用户标识 189 | "t" : "", 190 | "acData" : { 191 | "type" : "ACSCERR", //上报数据类型:资源加载异常 192 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 193 | "sTme" : "1591760073422", //事件上报时间 194 | "fileName" : "test.js", //文件名 195 | "resourceUri" : "http://ccedit.com/js/test.js", //资源地址 196 | "tagName" : "script", //标签类型 197 | "outerHTML" : "script ...", //标签内容 198 | } 199 | } 200 | ``` 201 | 202 | ### 4. Promise 异常数据 203 | 204 | ``` 205 | { 206 | "uuid": "F6A6C801B7197603", //用户标识 207 | "t" : "", 208 | "acData" : { 209 | "type" : "ACPRERR", //上报数据类型:Promise 异常 210 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 211 | "sTme" : "1591760073422", //事件上报时间 212 | "reason" : "reason" //异常说明 213 | } 214 | } 215 | ``` 216 | 217 | ### 5. 自定义事件数据 218 | 219 | ``` 220 | //自定义事件上报 221 | vue.$vueDataAc.setCustomAc({ 222 | cusKey: "click-button-001", 223 | cusVal: "1" 224 | }) 225 | 226 | { 227 | "uuid": "F6A6C801B7197603", //用户标识 228 | "t" : "", 229 | "acData" : { 230 | "type" : "ACCUSTOM", //上报数据类型:自定义事件 231 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 232 | "sTme" : "1591760073422", //事件上报时间 233 | "cusKey" : "click-button-001" //自定义事件key,用户定义 234 | "cusVal" :"1" //自定义事件值,用户定义 235 | } 236 | } 237 | ``` 238 | ### 6. Vue异常监控数据 239 | 240 | ``` 241 | { 242 | "uuid": "F6A6C801B7197603", //用户标识 243 | "t" : "", 244 | "acData" : { 245 | "type" : "ACVUERR", //上报数据类型:Vue异常监控 246 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 247 | "sTme" : "1591760073422", //事件上报时间 248 | "componentName" : "Button" //组件名 249 | "fileName" : "button.js" //组件文件 250 | "propsData" : "{}" //组件props 251 | "err" : "..." //错误堆栈 252 | "info" : "信息" //错误信息 253 | "msg" : "1" //异常摘要 254 | } 255 | } 256 | ``` 257 | ### 7. 点击事件监控数据 258 | 259 | ``` 260 | { 261 | "uuid": "F6A6C801B7197603", //用户标识 262 | "t" : "", //后端 用户标识/登录标识 默认为空,通过setUserToken设置 263 | "acData" : { 264 | "type" : "ACCLIK", //上报数据类型:点击事件监控 265 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 266 | "sTme" : "1591760073422", //事件上报时间 267 | "eId" : "" //元素id属性 268 | "className" : "login-form" //点击元素class属性 269 | "val" : "标题" //元素value或者innertext 270 | "attrs" : "{class:'...', placeholder:'...', type:'...'}" //元素所有属性对象 271 | } 272 | } 273 | ``` 274 | 275 | ### 8. 输入事件监控数据 276 | 277 | ``` 278 | { 279 | "uuid": "F6A6C801B7197603", //用户标识 280 | "t" : "", //后端 用户标识/登录标识 默认为空,通过setUserToken设置 281 | "acData" : { 282 | "type" : "ACINPUT", //上报数据类型:输入事件监控 283 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 284 | "sTme" : "1591760073422", //事件上报时间 285 | "eId" : "" //元素id属性 286 | "className" : "van-field__control" //元素class属性 287 | "val" : "0:111,638:11,395:1,327:,1742:5,214:55,207:555,175:5555" //时间:当前值,用逗号分隔,体现时间变化 288 | "attrs" : "{class:'...', placeholder:'...', type:'...'}" //元素所有属性对象 289 | } 290 | } 291 | ``` 292 | 293 | ### 9. 接口异常数据(包含 请求时间过长/自定义code/请求错误) 294 | 295 | ``` 296 | { 297 | "uuid": "F6A6C801B7197603", //用户标识 298 | "t" : "", 299 | "acData" : { 300 | "type" : "ACRERR", //上报数据类型:接口异常 301 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 302 | "sTme" : "1591760073422", //事件上报时间 303 | "errSubType" : "http/time/custom" //异常类型:【time: 请求时间过长】【custom: 自定义code】【http:请求错误】 304 | "responseURL" : "/static/push" //请求接口 305 | "method" : "GET" //请求方式 306 | "readyState" : 4 //xhr.readyState状态码 307 | "status" : "404" //请求状态码 308 | "statusText" : "not found" //错误描述 309 | "requestTime" : 3000 //请求耗时 310 | "response" : "{...}" //接口响应摘要,截取前100个字符 311 | "query" : "{}" //请求参数,用 openXhrQuery 配置打开,注意用户信息泄露 312 | } 313 | } 314 | ``` 315 | 316 | ### 10. 页面性能监控数据 317 | 318 | ``` 319 | { 320 | "uuid": "F6A6C801B7197603", //用户标识 321 | "t" : "", 322 | "acData" : { 323 | "type" : "ACTIME", //上报数据类型:页面性能监控 324 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 325 | "sTme" : "1591760073422", //事件上报时间 326 | "WT" : 1000 //白屏时间 327 | "TCP" : 1000 //TCP连接耗时 328 | "ONL" : 1000 //执行onload事件耗时 329 | "ALLRT" : 1000 //所有请求耗时 330 | "TTFB" : 1000 //TTFB读取页面第一个字节的时间 331 | "DNS" : 1000 //DNS查询时间 332 | } 333 | } 334 | ``` 335 | ### 11. Vue组件性能监控数据 336 | 337 | ``` 338 | { 339 | "uuid": "F6A6C801B7197603", //用户标识 340 | "t" : "", 341 | "acData" : { 342 | "type" : "ACCOMP", //上报数据类型:页面性能监控 343 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址 344 | "sTme" : "1591760073422", //事件上报时间 345 | "componentsTimes" : [ //渲染超时组件列表 346 | '组件名': [1000,1200,1090] 347 | ] 348 | } 349 | } 350 | ``` 351 | 352 | ## TODO: 353 | 354 | - [x] 异常监控 355 | - [x] 代码异常 356 | - [x] 资源加载异常 357 | - [x] promise异常 358 | - [x] Vue异常 359 | - [x] 请求异常(慢请求,超时,错误) 360 | - [x] axios异常 361 | 362 | - [x] 用户行为监控 363 | - [x] 点击事件 364 | - [x] 输入事件 365 | - [x] 自定义事件 366 | - [x] 页面访问事件 367 | 368 | - [x] 数据上报 369 | - [x] 图片上报 370 | - [x] 接口上报 371 | - [x] 手动上报 372 | 373 | - [x] 页面性能上报 374 | - [x] performance 375 | - [x] 组件性能上报 376 | 377 | - [x] 留存 378 | - [x] 访问时间 379 | - [x] 访问间隔 380 | 381 | - [x] 开关 382 | - [x] openPage 页面访问信息采集开关 383 | - [x] openSourceErr 资源加载异常采集 384 | - [x] openPromiseErr promise异常采集 385 | - [x] openCodeErr 是否开启代码异常采集 386 | - [x] openVueErr 是否开启Vue异常监控 387 | - [x] openSourceErr 是否开启资源加载异常采集 388 | - [x] openPromiseErr 是否开启promise异常采集 389 | - [x] openClick 是否开启点击数据采集 390 | - [x] openInput 是否开启输入数据采集 391 | - [x] openXhrQuery 是否采集接口异常时的参数params 392 | - [x] openXhrHock 是否开启xhr异常采集 393 | - [x] openPerformance 是否开启页面性能采集 394 | - [x] openComponent 组件性能采集 395 | 396 | 397 | - [x] npm自动发布 398 | - [x] 后端日志关联机制 399 | - [x] eslint 400 | - [x] docs 401 | - [x] demo 402 | - [x] 文章 403 | 404 | 405 | ## Q&A 406 | **1. 我需要采集用户行为吗?** 407 | 408 | 用户行为相关数据,我认为对产品有益,可以用于分析转化,页面热点图等 409 | 根据数据对产品进行调整。所以看你的产品类型 2C 的产品一般有这样的需求 410 | 411 | **2. 我需要监控页面异常吗?** 412 | 413 | 从前端角度是有必要的,用户遇到问题,经过问题上报,汇总,最终分配到你,时间不可控 414 | 能在第一时间对端上的问题进行告警,会大大提高解决问题的效率 415 | 所以我认为需要有一个监控系统作为生产安全的兜底方案 416 | 417 | **3. 我需要采集页面性能&组件性能吗?** 418 | 419 | 页面性能组件性能,我建议开启,生产测试环境可能因为数据不相同,会有差异性bug 420 | 可能会导致组件渲染慢,影响体验 421 | 但是前提是要把阈值调大,以免数据过多,大量数据上报,会降低对报警的敏感度 422 | 423 | **4. 数据采集后如何进行整理分析?** 424 | 425 | 我们的数据上报分为两种,接口和图片 426 | 其实不论哪一种方式,最终都要将数据本地化,持久化。 427 | 可以问一下公司后端的同事,他们的数据怎么分析,对接他们的上报接口就可以。 428 | 一般这样的日志搜集分析,会用到 ELK 系统。没有的话让运维帮忙搭建一套。 429 | 通过接口将上报的数据存储到本地文件或数据库中。或是通过图片上报,将数据存储在nginx中 430 | 然后用ELK对接日志即可。ELK有提供查询API,你可以做一套轮训告警系统 431 | 432 | **5. 如何将前端日志与后台日志关联起来?** 433 | 434 | 前端日志的唯一标识是uuid,后端唯一标识可以通过 setUserToken方法将用户唯一 id 和 uuid 做关联 435 | 436 | 437 | **6. 我该如何将他应用到生产项目中?** 438 | 439 | 建议你分批次,分功能,做足够量的测试之后,逐步打开开关上线。 440 | 我只能保证在我的场景下可以正常使用,但是不同的产品,不同的用户场景,不能百分百保证 441 | 442 | 443 | ## 设计思路 444 | 445 | ### 1. 页面访问采集 446 | 作为页面级访问的采集,我们在beforeCreate中注入了根组件监控, 447 | 当根组件开始渲染时,上报当前页面的地址信息,同时会将此次记录本地存储,页面跳转后,会附加到from字段。 448 | 在多页面应用的场景下,该方式保证了上报,但如果是多入口嵌套vue-router的情况下,会触发第二种监控, 449 | 路由监控,插件会监控你的$route变化,并记录上报。在没有使用vue-router的情况下,只会上报页面的访问信息 450 | 451 | ## change log 452 | ### 2.0.9: 453 | **bugfix:** 454 | 1. 修复页面刷新数据未上报问题 455 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const zlib = require('zlib') 4 | const terser = require('terser') 5 | const rollup = require('rollup') 6 | const configs = require('./config') 7 | 8 | if (!fs.existsSync('dist')) { 9 | fs.mkdirSync('dist') 10 | } 11 | 12 | function build(builds) { 13 | let built = 0 14 | const total = builds.length 15 | const next = () => { 16 | buildEntry(builds[built]) 17 | .then(() => { 18 | built++ 19 | if (built < total) { 20 | next() 21 | } 22 | }) 23 | .catch(logError) 24 | } 25 | 26 | next() 27 | } 28 | 29 | build(configs) 30 | 31 | function buildEntry({input, output}) { 32 | const {file, banner} = output 33 | const isProd = /min\.js$/.test(file) 34 | return rollup 35 | .rollup(input) 36 | .then(bundle => bundle.generate(output)) 37 | .then(bundle => { 38 | // console.log(bundle) 39 | const code = bundle.output[0].code 40 | if (isProd) { 41 | const minified = terser.minify(code, { 42 | toplevel: true, 43 | output: { 44 | ascii_only: true 45 | }, 46 | compress: { 47 | pure_funcs: ['makeMap'] 48 | } 49 | }).code 50 | return write(file, minified, true) 51 | } else { 52 | return write(file, code) 53 | } 54 | }) 55 | } 56 | 57 | function write(dest, code, zip) { 58 | return new Promise((resolve, reject) => { 59 | function report(extra) { 60 | console.log( 61 | blue(path.relative(process.cwd(), dest)) + 62 | ' ' + 63 | getSize(code) + 64 | (extra || '') 65 | ) 66 | resolve() 67 | } 68 | 69 | fs.writeFile(dest, code, err => { 70 | if (err) return reject(err) 71 | if (zip) { 72 | zlib.gzip(code, (err, zipped) => { 73 | if (err) return reject(err) 74 | report(' (gzipped: ' + getSize(zipped) + ')') 75 | }) 76 | } else { 77 | report() 78 | } 79 | }) 80 | }) 81 | } 82 | 83 | function getSize(code) { 84 | return (code.length / 1024).toFixed(2) + 'kb' 85 | } 86 | 87 | function logError(e) { 88 | console.log(e) 89 | } 90 | 91 | function blue(str) { 92 | return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m' 93 | } 94 | -------------------------------------------------------------------------------- /build/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const buble = require('rollup-plugin-buble') // es6 转 es5 4 | const eslint = require('rollup-plugin-eslint').eslint; 5 | const flow = require('rollup-plugin-flow-no-whitespace') //忽略报错使用flow 6 | const cjs = require('rollup-plugin-commonjs') // 将非ES6语法的包转为ES6可用 7 | const node = require('rollup-plugin-node-resolve') // 帮助寻找node_modules里的包 8 | const replace = require('rollup-plugin-replace') //replace插件的用途是在打包时动态替换代码中的内容 9 | const version = process.env.VERSION || require('../package.json').version 10 | const banner = 11 | `/*! 12 | * vue-dataAc v${version} 13 | * (c) ${new Date().getFullYear()} Cc-Edit 14 | * @license MIT 15 | */` 16 | 17 | const resolve = _path => path.resolve(__dirname, '../', _path) 18 | 19 | 20 | module.exports = [ 21 | // browser dev 22 | { 23 | file: resolve('dist/vue-dataAc.js'), 24 | format: 'umd', 25 | env: 'development' 26 | }, 27 | { 28 | file: resolve('dist/vue-dataAc.min.js'), 29 | format: 'umd', 30 | env: 'production' 31 | }, 32 | { 33 | file: resolve('dist/vue-dataAc.common.js'), 34 | format: 'cjs' 35 | }, 36 | { 37 | file: resolve('dist/vue-dataAc.esm.js'), 38 | format: 'es' 39 | }, 40 | { 41 | file: resolve('dist/vue-dataAc.esm.browser.js'), 42 | format: 'es', 43 | env: 'development', 44 | transpile: false 45 | }, 46 | { 47 | file: resolve('dist/vue-dataAc.esm.browser.min.js'), 48 | format: 'es', 49 | env: 'production', 50 | transpile: false 51 | } 52 | ].map(genConfig) 53 | 54 | function genConfig(opts) { 55 | const config = { 56 | input: { 57 | input: resolve('src/index.js'), 58 | plugins: [ 59 | eslint(), 60 | flow(), 61 | node(), 62 | cjs(), 63 | replace({ 64 | __VERSION__: version 65 | }) 66 | ] 67 | }, 68 | output: { 69 | file: opts.file, 70 | format: opts.format, 71 | banner, 72 | name: 'VueDataAc' 73 | } 74 | } 75 | 76 | if (opts.env) { 77 | config.input.plugins.unshift(replace({ 78 | 'process.env.NODE_ENV': JSON.stringify(opts.env) 79 | })) 80 | } 81 | 82 | if (opts.transpile !== false) { 83 | config.input.plugins.push(buble()) 84 | } 85 | 86 | return config 87 | } -------------------------------------------------------------------------------- /dist/vue-dataAc.esm.browser.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-dataAc v2.0.8 3 | * (c) 2020 adminV 4 | * @license MIT 5 | */ 6 | const t={storeVer:"2.0.8",storeInput:"ACINPUT",storePage:"ACPAGE",storeClick:"ACCLIK",storeReqErr:"ACRERR",storeTiming:"ACTIME",storeCodeErr:"ACCERR",storeCustom:"ACCUSTOM",storeSourceErr:"ACSCERR",storePrmseErr:"ACPRERR",storeCompErr:"ACCOMP",storeVueErr:"ACVUERR",userSha:"vue_ac_userSha",useImgSend:!0,useStorage:!0,maxDays:365,openInput:!0,openCodeErr:!0,openClick:!0,openXhrQuery:!0,openXhrHock:!0,openPerformance:!0,openPage:!0,openVueErr:!0,openSourceErr:!0,openPromiseErr:!0,openComponent:!0,maxComponentLoadTime:1e3,openXhrTimeOut:!0,maxRequestTime:1e4,customXhrErrCode:"",selector:"input",ignoreInputType:["password","file"],classTag:"",maxHelpfulCount:5,imageUrl:"https://data.ccedit.com/lib/image/ac.png",postUrl:"https://data.ccedit.com/logStash/push",openReducer:!1,sizeLimit:20,cacheEventStorage:"ac_cache_data",manualReport:!1};function e(t){return!(0===t&&"0"===t||void 0!==t&&null!=t&&"null"!==t&&""!==t)}function o(t){for(const e in t)return!1;return!0}function s(t){const e=t.attributes?t.attributes.length:0,o={};if(e>0)for(let s=0;s1?decodeURIComponent(t[2]):null}}function a(){const t=new Date;return{timeStr:`${t.getFullYear()}/${t.getMonth()+1}/${t.getDate()} ${t.getHours()}:${t.getMinutes()}:${t.getSeconds()}`,timeStamp:t.getTime()}}function r(t){const e=t.toString();let o=t.stack?t.stack.replace(/\n/gi,"").replace(/\bat\b/gi,"@").replace(/\?[^:]+/gi,"").replace(/^\s*|\s*$/g,"").split("@").slice(0,5).join("&&"):"";return o.indexOf(e)<0&&(o=e+"@"+o),o}let c;class p{constructor(s={},a={}){const r=function(t,e){const o={},s=Object.keys(e);for(let n=0;n{e(t[o])&&(s=!1)}),t.useImgSend){if(e(t.imageUrl))return!1}else if(e(t.postUrl))return!1;return(!t.openInput||!e(t.selector))&&((!t.useStorage||void 0!==window.localStorage)&&s)}(r))return void(this.installed=!1);this.installed=!0,this._options=r,this._vue_=a,c=this,this._uuid=i(this._options,this._options.userSha),e(this._uuid)&&(this._uuid=function(t=16,e=16){const o="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""),s=[];for(let n=0;n=t.$vueDataAc._options.maxComponentLoadTime&&(e(t.$vueDataAc._componentsTime[i])&&(t.$vueDataAc._componentsTime[i]=[]),t.$vueDataAc._componentsTime[i].push(c));0==--t.$vueDataAc._componentTimeCount&&!o(t.$vueDataAc._componentsTime)&&(this._setAcData(t.$vueDataAc._options.storeCompErr,{componentsTimes:t.$vueDataAc._componentsTime}),t.$vueDataAc._componentsTime={})}_formatInputEvent(t){const n=window.event||t,i=n.srcElement?n.srcElement:n.target,{id:r,className:p,value:u,innerText:m}=i,h=s(i),l=u||m,_=a().timeStamp;let d="";try{d=JSON.stringify(h)}catch(t){d=`${r}-${p}`}let f=c._inputCacheData[d];f=e(f)||o(f)?{value:"0:"+l,timeStamp:_}:{value:`${f.value},${parseInt(_-f.timeStamp)}:${l}`,timeStamp:_},c._inputCacheData[d]=f}_formatBlurEvent(t){const o=window.event||t,n=o.srcElement?o.srcElement:o.target,{id:i,className:a}=n,r=s(n);let p="";try{p=JSON.stringify(r)}catch(t){p=`${i}-${a}`}const u=c._inputCacheData[p];e(u)||(c._inputCacheData[p]=null,c._setAcData(c._options.storeInput,{eId:i,className:a,val:u.value,attrs:r}))}_mixinRouterWatch(t={},s={},a){let r="",c={},p="",u={},m="";if(a){if(r=t.fullPath||t.path||t.name,c=o(t.params)?t.query:t.params,p=s.fullPath||s.path||s.name,u=o(s.params)?s.query:s.params,m=this._lastRouterStr,m===`${r}-${JSON.stringify(c)}`)return}else m=i(this._options,"_vueac_"+this._options.storePage)||"",r=window.location.href,c={search:window.location.search},p=m,u={};a&&(e(r)||e(p))||(this._lastRouterStr=`${r}-${JSON.stringify(c)}`,n(this._options,"_vueac_"+this._options.storePage,`${r}-${JSON.stringify(c)}`),this._setAcData(this._options.storePage,{toPath:r,toParams:c,fromPath:p,formParams:u}),this._options.openReducer&&!this._options.manualReport&&this.postAcData&&this.postAcData())}_initClickAc(){document.addEventListener("click",t=>{const o=window.event||t,n=function t(o,s,n=0){if(Object.prototype.toString.call(o)===Object.prototype.toString.call(document))return null;const i=o&&o.parentNode,{className:a="",id:r}=o,{classTag:c,maxHelpfulCount:p}=s;return e(c)?n>p?null:e(a)&&e(r)?e(i)?null:t(i,s,++n):o:a.indexOf(c)<0?e(i)||n>p?null:t(i,s,++n):o}(o.srcElement?o.srcElement:o.target,this._options);if(!e(n)){const{className:t,id:e,value:o,innerText:i}=n,a=s(n);this._setAcData(this._options.storeClick,{eId:e,className:t,val:(o||i).substr(0,20),attrs:a})}})}_initXhrErrAc(){const t=XMLHttpRequest.prototype.open,e=XMLHttpRequest.prototype.send,o=XMLHttpRequest.onreadystatechange;this._proxyXhrObj={open:function(){return this._ac_method=(arguments[0]||[])[0],t&&t.apply(this,arguments)},send:function(){return this._ac_send_time=a().timeStamp,this._ac_post_data=(arguments[0]||[])[0]||"",this.addEventListener("error",(function(t){c._formatXhrErrorData(t.target)})),this.onreadystatechange=function(t){c._formatXhrErrorData(t.target),o&&o.apply(this,arguments)},e&&e.apply(this,arguments)}},XMLHttpRequest.prototype.open=this._proxyXhrObj.open,XMLHttpRequest.prototype.send=this._proxyXhrObj.send}_formatXhrErrorData(t){const o=t,{method:s,send_time:n=0,post_data:i={},readyState:r}=o;if(4===r){const{status:t,statusText:p,response:u,responseURL:m}=o,h=a().timeStamp,l=h-(n||h),{openXhrTimeOut:_,storeReqErr:d,customXhrErrCode:f,openXhrQuery:v}=c._options,g=l>c._options.maxRequestTime,T=!(t>=200&&t<208)&&0!==t&&302!==t,E=!e(f)&&""+(u&&u.code)===f,D=!e(m)&&m===c._options.postUrl;(_&&g||T||E)&&!D&&c._setAcData(d,{responseURL:m,method:s,isHttpErr:T,isCustomErr:E,readyState:r,status:t,statusText:p,requestTime:l,response:(""+u).substr(0,100),query:v?i:""})}}_initPerformance(){if(window.performance){const o=(window.performance||{}).timing;if(!e(o)){var t={WT:o.responseStart-o.navigationStart,TCP:o.connectEnd-o.connectStart,ONL:o.loadEventEnd-o.loadEventStart,ALLRT:o.responseEnd-o.requestStart,TTFB:o.responseStart-o.navigationStart,DNS:o.domainLookupEnd-o.domainLookupStart};this._setAcData(this._options.storeTiming,t)}}}_initVueErrAc(){this._vue_&&this._vue_.config&&(this._vue_.config.errorHandler=(t={},e,o)=>{const s=e._isVue?e.$options&&e.$options.name||e.$options&&e.$options._componentTag:e.name,n=e._isVue&&e.$options&&e.$options.__file?e.$options&&e.$options.__file:"",i=e.$options&&e.$options.propsData;this._setAcData(this._options.storeVueErr,{componentName:s,fileName:n,propsData:i,info:o,msg:t.message||"",stack:r(t)})})}_initCodeErrAc(){window.onerror=(t,o,s,n,i)=>{if(e(o)&&"Script error."===t)return!1;const a={msg:t,line:s,col:n};a.col=n||window.event&&window.event.errorCharacter||0,setTimeout(()=>{if(i&&i.stack)a.err=i.stack.toString();else if(arguments.callee){let t=[],e=arguments.callee.caller,o=3;for(;e&&--o>0&&(t.push(e.toString()),e!==e.caller);)e=e.caller;t=t.join(","),a.err=t}else a.err="script err";this._setAcData(this._options.storeCodeErr,a)},0)}}_initSourceErrAc(){window.addEventListener("error",t=>{if("[object Event]"===[].toString.call(t,t)){const o=t.target||t.srcElement||t.originalTarget||{},{href:s,src:n,currentSrc:i,localName:a}=o,r=o.tagName||a;let c=o.outerHTML;const p=s||n;if("IMG"===r&&!e(o.onerror))return!1;c&&c.length>200&&(c=c.slice(0,200)),this._setAcData(this._options.storeSourceErr,{tagName:r,outerHTML:c,resourceUri:p,currentSrc:i})}})}_initPromiseErrAc(){window.addEventListener("unhandledrejection",t=>{this._setAcData(this._options.storePrmseErr,{reason:t.reason||"unknown"}),t.preventDefault()})}_setAcData(t,e){const o={uuid:this._uuid,t:this._userToken};switch(t){case this._options.storePage:{const{toPath:t,toParams:s,fromPath:n,formParams:i}=e,r=this._pageInTime,c=a().timeStamp;this._pageInTime=c,o.acData={type:this._options.storePage,sTme:c,fromPath:n,formParams:i,toPath:t,toParams:s,inTime:r,outTime:c}}break;case this._options.storeInput:{const{eId:t,className:s,val:n,attrs:i}=e;o.acData={type:this._options.storeInput,path:window.location.href,sTme:a().timeStamp,eId:t,className:s,val:n,attrs:i}}break;case this._options.storeClick:{const{eId:t,className:s,val:n,attrs:i}=e;o.acData={type:this._options.storeClick,path:window.location.href,sTme:a().timeStamp,eId:t,className:s,val:n,attrs:i}}break;case this._options.storeReqErr:{const{responseURL:t,method:s,isHttpErr:n,isCustomErr:i,readyState:r,status:c,statusText:p,requestTime:u,response:m,query:h}=e;o.acData={type:this._options.storeReqErr,path:window.location.href,sTme:a().timeStamp,errSubType:n?"http":i?"custom":"time",responseURL:t,method:s,readyState:r,status:c,statusText:p,requestTime:u,response:m,query:h}}break;case this._options.storeVueErr:{const{componentName:t,fileName:s,propsData:n,info:i,msg:r,stack:c}=e;o.acData={type:this._options.storeVueErr,path:window.location.href,sTme:a().timeStamp,componentName:t,fileName:s,propsData:n,info:i,msg:r,err:c}}break;case this._options.storeCodeErr:{const{msg:t,line:s,col:n,err:i}=e;o.acData={type:this._options.storeCodeErr,path:window.location.href,sTme:a().timeStamp,msg:t,line:s,col:n,err:i}}break;case this._options.storeSourceErr:{const{tagName:t,outerHTML:s,resourceUri:n,currentSrc:i}=e;o.acData={type:this._options.storeSourceErr,path:window.location.href,sTme:a().timeStamp,fileName:i,resourceUri:n,tagName:t,outerHTML:s}}break;case this._options.storePrmseErr:{const{reason:t}=e;o.acData={type:this._options.storePrmseErr,path:window.location.href,sTme:a().timeStamp,reason:t}}break;case this._options.storeCustom:{const{cusKey:t,cusVal:s}=e;o.acData={type:this._options.storeCustom,path:window.location.href,sTme:a().timeStamp,cusKey:t,cusVal:s}}break;case this._options.storeTiming:{const{WT:t,TCP:s,ONL:n,ALLRT:i,TTFB:r,DNS:c}=e;o.acData={type:this._options.storeTiming,path:window.location.href,sTme:a().timeStamp,WT:t,TCP:s,ONL:n,ALLRT:i,TTFB:r,DNS:c}}break;case this._options.storeCompErr:{const{componentsTimes:t}=e;o.acData={type:this._options.storeCompErr,path:window.location.href,sTme:a().timeStamp,componentsTimes:t}}}this._acData.push(o),this._options.openReducer?(n(this._options,this._options.cacheEventStorage,JSON.stringify(this._acData)),!this._options.manualReport&&this._options.sizeLimit&&this._acData.length>=this._options.sizeLimit&&(this._vue_&&this._vue_.$nextTick?this._vue_.$nextTick(()=>{this.postAcData()}):this.postAcData())):this._vue_&&this._vue_.$nextTick?this._vue_.$nextTick(()=>{this.postAcData()}):this.postAcData()}setCustomAc(t){const{cusKey:e="custom",cusVal:o=""}=t;this._setAcData(this._options.storeCustom,{cusKey:e,cusVal:o})}postAcData(){if(e(this._acData)||0===this._acData.length)return;const t=JSON.stringify(this._acData);var o,s;this._options.useImgSend?(new Image).src=`${this._options.imageUrl}?acError=${t}`:function(t={}){let e,o;t.type=(t.type||"GET").toUpperCase(),t.dataType=t.dataType||"json",t.async=t.async||!0,t.data&&(o=t.data),window.XMLHttpRequest?(e=new XMLHttpRequest,e.overrideMimeType&&e.overrideMimeType("text/xml")):e=new ActiveXObject("Microsoft.XMLHTTP"),"GET"===t.type?(e.open("GET",t.url+"?"+o,t.async),e.send(null)):"POST"===t.type&&(e.open("POST",t.url,t.async),e.setRequestHeader("Content-Type","application/json; charset=UTF-8"),o?e.send(o):e.send())}({type:"POST",dataType:"json",contentType:"application/json",data:t,url:this._options.postUrl}),this._acData=[],o=this._options,s=this._options.cacheEventStorage,o.useStorage?window.localStorage.removeItem(s):n(o,s,"",-1)}setUserToken(t){this._userToken=t}}p.install=(t,e)=>function t(e,o,s){t.installed||(t.installed=!0,e.mixin({watch:{$route(t,e){this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openPage&&this.$vueDataAc._mixinRouterWatch(t,e,!0)}},beforeCreate:function(){this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openComponent&&this.$vueDataAc._mixinComponentsPerformanceStart(this),this._uid===this.$root._uid&&this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openPage&&this.$vueDataAc._mixinRouterWatch(null,null,!1)},beforeDestroy(){this.$vueDataAc&&this.$vueDataAc.installed&&this._uid===this.$root._uid&&this.$vueDataAc&&this.$vueDataAc.postAcData()},mounted(){this.$vueDataAc&&this.$vueDataAc.installed&&(this.$vueDataAc._options.openInput&&(this.$vueDataAc._componentLoadCount++,this.$nextTick((function(){0==--this.$vueDataAc._componentLoadCount&&this.$vueDataAc._mixinInputEvent(this)}))),this.$vueDataAc._options.openComponent&&this.$nextTick((function(){this.$vueDataAc._mixinComponentsPerformanceEnd(this)})))},beforeUpdate(){this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openComponent&&this.$vueDataAc._mixinComponentsPerformanceStart(this)},updated(){this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openComponent&&this.$nextTick((function(){this.$vueDataAc._mixinComponentsPerformanceEnd(this)}))}}),e.prototype.$vueDataAc=new s(o,e))}(t,e,p),p.version="2.0.8";export default p; -------------------------------------------------------------------------------- /dist/vue-dataAc.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-dataAc v2.0.8 3 | * (c) 2020 adminV 4 | * @license MIT 5 | */ 6 | var t,e;t=this,e=function(){"use strict";var t,e={storeVer:"2.0.8",storeInput:"ACINPUT",storePage:"ACPAGE",storeClick:"ACCLIK",storeReqErr:"ACRERR",storeTiming:"ACTIME",storeCodeErr:"ACCERR",storeCustom:"ACCUSTOM",storeSourceErr:"ACSCERR",storePrmseErr:"ACPRERR",storeCompErr:"ACCOMP",storeVueErr:"ACVUERR",userSha:"vue_ac_userSha",useImgSend:!0,useStorage:!0,maxDays:365,openInput:!0,openCodeErr:!0,openClick:!0,openXhrQuery:!0,openXhrHock:!0,openPerformance:!0,openPage:!0,openVueErr:!0,openSourceErr:!0,openPromiseErr:!0,openComponent:!0,maxComponentLoadTime:1e3,openXhrTimeOut:!0,maxRequestTime:1e4,customXhrErrCode:"",selector:"input",ignoreInputType:["password","file"],classTag:"",maxHelpfulCount:5,imageUrl:"https://data.ccedit.com/lib/image/ac.png",postUrl:"https://data.ccedit.com/logStash/push",openReducer:!1,sizeLimit:20,cacheEventStorage:"ac_cache_data",manualReport:!1};function o(t){return!(0===t&&"0"===t||void 0!==t&&null!=t&&"null"!==t&&""!==t)}function r(t){for(var e in t)return!1;return!0}function i(t){var e=t.attributes?t.attributes.length:0,o={};if(e>0)for(var r=0;r1?decodeURIComponent(o[2]):null}function s(){var t=new Date;return{timeStr:t.getFullYear()+"/"+(t.getMonth()+1)+"/"+t.getDate()+" "+t.getHours()+":"+t.getMinutes()+":"+t.getSeconds(),timeStamp:t.getTime()}}function p(t){var e=t.toString(),o=t.stack?t.stack.replace(/\n/gi,"").replace(/\bat\b/gi,"@").replace(/\?[^:]+/gi,"").replace(/^\s*|\s*$/g,"").split("@").slice(0,5).join("&&"):"";return o.indexOf(e)<0&&(o=e+"@"+o),o}var c=function(i,s){void 0===i&&(i={}),void 0===s&&(s={});var p=function(t,e){for(var o={},r=Object.keys(e),i=0;i=t.$vueDataAc._options.maxComponentLoadTime&&(o(t.$vueDataAc._componentsTime[a])&&(t.$vueDataAc._componentsTime[a]=[]),t.$vueDataAc._componentsTime[a].push(p)),0==--t.$vueDataAc._componentTimeCount&&!r(t.$vueDataAc._componentsTime)&&(this._setAcData(t.$vueDataAc._options.storeCompErr,{componentsTimes:t.$vueDataAc._componentsTime}),t.$vueDataAc._componentsTime={})}},c.prototype._formatInputEvent=function(e){var a=window.event||e,n=a.srcElement?a.srcElement:a.target,p=n.id,c=n.className,u=n.value,m=n.innerText,h=i(n),l=u||m,_=s().timeStamp,d="";try{d=JSON.stringify(h)}catch(e){d=p+"-"+c}var v=t._inputCacheData[d];v=o(v)||r(v)?{value:"0:"+l,timeStamp:_}:{value:v.value+","+parseInt(_-v.timeStamp)+":"+l,timeStamp:_},t._inputCacheData[d]=v},c.prototype._formatBlurEvent=function(e){var r=window.event||e,a=r.srcElement?r.srcElement:r.target,n=a.id,s=a.className,p=i(a),c="";try{c=JSON.stringify(p)}catch(e){c=n+"-"+s}var u=t._inputCacheData[c];o(u)||(t._inputCacheData[c]=null,t._setAcData(t._options.storeInput,{eId:n,className:s,val:u.value,attrs:p}))},c.prototype._mixinRouterWatch=function(t,e,i){void 0===t&&(t={}),void 0===e&&(e={});var s="",p={},c="",u={},m="";if(i){if(s=t.fullPath||t.path||t.name,p=r(t.params)?t.query:t.params,c=e.fullPath||e.path||e.name,u=r(e.params)?e.query:e.params,(m=this._lastRouterStr)===s+"-"+JSON.stringify(p))return}else m=n(this._options,"_vueac_"+this._options.storePage)||"",s=window.location.href,p={search:window.location.search},c=m,u={};i&&(o(s)||o(c))||(this._lastRouterStr=s+"-"+JSON.stringify(p),a(this._options,"_vueac_"+this._options.storePage,s+"-"+JSON.stringify(p)),this._setAcData(this._options.storePage,{toPath:s,toParams:p,fromPath:c,formParams:u}),this._options.openReducer&&!this._options.manualReport&&this.postAcData&&this.postAcData())},c.prototype._initClickAc=function(){var t=this;document.addEventListener("click",(function(e){var r=window.event||e,a=function t(e,r,i){if(void 0===i&&(i=0),Object.prototype.toString.call(e)===Object.prototype.toString.call(document))return null;var a=e&&e.parentNode,n=e.className;void 0===n&&(n="");var s=e.id,p=r.classTag,c=r.maxHelpfulCount;return o(p)?i>c?null:o(n)&&o(s)?o(a)?null:t(a,r,++i):e:n.indexOf(p)<0?o(a)||i>c?null:t(a,r,++i):e}(r.srcElement?r.srcElement:r.target,t._options);if(!o(a)){var n=a.className,s=a.id,p=a.value,c=a.innerText,u=i(a);t._setAcData(t._options.storeClick,{eId:s,className:n,val:(p||c).substr(0,20),attrs:u})}}))},c.prototype._initXhrErrAc=function(){var e=XMLHttpRequest.prototype.open,o=XMLHttpRequest.prototype.send,r=XMLHttpRequest.onreadystatechange;this._proxyXhrObj={open:function(){return this._ac_method=(arguments[0]||[])[0],e&&e.apply(this,arguments)},send:function(){return this._ac_send_time=s().timeStamp,this._ac_post_data=(arguments[0]||[])[0]||"",this.addEventListener("error",(function(e){t._formatXhrErrorData(e.target)})),this.onreadystatechange=function(e){t._formatXhrErrorData(e.target),r&&r.apply(this,arguments)},o&&o.apply(this,arguments)}},XMLHttpRequest.prototype.open=this._proxyXhrObj.open,XMLHttpRequest.prototype.send=this._proxyXhrObj.send},c.prototype._formatXhrErrorData=function(e){var r=e,i=r.method,a=r.send_time;void 0===a&&(a=0);var n=r.post_data;void 0===n&&(n={});var p=r.readyState;if(4===p){var c=r.status,u=r.statusText,m=r.response,h=r.responseURL,l=s().timeStamp,_=l-(a||l),d=t._options,v=d.openXhrTimeOut,f=d.storeReqErr,g=d.customXhrErrCode,T=d.openXhrQuery,E=_>t._options.maxRequestTime,D=!(c>=200&&c<208)&&0!==c&&302!==c,S=!o(g)&&""+(m&&m.code)===g,y=!o(h)&&h===t._options.postUrl;(v&&E||D||S)&&!y&&t._setAcData(f,{responseURL:h,method:i,isHttpErr:D,isCustomErr:S,readyState:p,status:c,statusText:u,requestTime:_,response:(""+m).substr(0,100),query:T?n:""})}},c.prototype._initPerformance=function(){if(window.performance){var t=(window.performance||{}).timing;if(!o(t)){var e={WT:t.responseStart-t.navigationStart,TCP:t.connectEnd-t.connectStart,ONL:t.loadEventEnd-t.loadEventStart,ALLRT:t.responseEnd-t.requestStart,TTFB:t.responseStart-t.navigationStart,DNS:t.domainLookupEnd-t.domainLookupStart};this._setAcData(this._options.storeTiming,e)}}},c.prototype._initVueErrAc=function(){var t=this;this._vue_&&this._vue_.config&&(this._vue_.config.errorHandler=function(e,o,r){void 0===e&&(e={});var i=o._isVue?o.$options&&o.$options.name||o.$options&&o.$options._componentTag:o.name,a=o._isVue&&o.$options&&o.$options.__file?o.$options&&o.$options.__file:"",n=o.$options&&o.$options.propsData;t._setAcData(t._options.storeVueErr,{componentName:i,fileName:a,propsData:n,info:r,msg:e.message||"",stack:p(e)})})},c.prototype._initCodeErrAc=function(){var t=arguments,e=this;window.onerror=function(r,i,a,n,s){if(o(i)&&"Script error."===r)return!1;var p={msg:r,line:a,col:n};p.col=n||window.event&&window.event.errorCharacter||0,setTimeout((function(){if(s&&s.stack)p.err=s.stack.toString();else if(t.callee){for(var o=[],r=t.callee.caller,i=3;r&&--i>0&&(o.push(r.toString()),r!==r.caller);)r=r.caller;o=o.join(","),p.err=o}else p.err="script err";e._setAcData(e._options.storeCodeErr,p)}),0)}},c.prototype._initSourceErrAc=function(){var t=this;window.addEventListener("error",(function(e){if("[object Event]"===[].toString.call(e,e)){var r=e.target||e.srcElement||e.originalTarget||{},i=r.href,a=r.src,n=r.currentSrc,s=r.localName,p=r.tagName||s,c=r.outerHTML,u=i||a;if("IMG"===p&&!o(r.onerror))return!1;c&&c.length>200&&(c=c.slice(0,200)),t._setAcData(t._options.storeSourceErr,{tagName:p,outerHTML:c,resourceUri:u,currentSrc:n})}}))},c.prototype._initPromiseErrAc=function(){var t=this;window.addEventListener("unhandledrejection",(function(e){t._setAcData(t._options.storePrmseErr,{reason:e.reason||"unknown"}),e.preventDefault()}))},c.prototype._setAcData=function(t,e){var o=this,r={uuid:this._uuid,t:this._userToken};switch(t){case this._options.storePage:var i=e.toPath,n=e.toParams,p=e.fromPath,c=e.formParams,u=this._pageInTime,m=s().timeStamp;this._pageInTime=m,r.acData={type:this._options.storePage,sTme:m,fromPath:p,formParams:c,toPath:i,toParams:n,inTime:u,outTime:m};break;case this._options.storeInput:var h=e.eId,l=e.className,_=e.val,d=e.attrs;r.acData={type:this._options.storeInput,path:window.location.href,sTme:s().timeStamp,eId:h,className:l,val:_,attrs:d};break;case this._options.storeClick:var v=e.eId,f=e.className,g=e.val,T=e.attrs;r.acData={type:this._options.storeClick,path:window.location.href,sTme:s().timeStamp,eId:v,className:f,val:g,attrs:T};break;case this._options.storeReqErr:var E=e.responseURL,D=e.method,S=e.isHttpErr,y=e.isCustomErr,A=e.readyState,C=e.status,$=e.statusText,w=e.requestTime,P=e.response,R=e.query;r.acData={type:this._options.storeReqErr,path:window.location.href,sTme:s().timeStamp,errSubType:S?"http":y?"custom":"time",responseURL:E,method:D,readyState:A,status:C,statusText:$,requestTime:w,response:P,query:R};break;case this._options.storeVueErr:var x=e.componentName,k=e.fileName,L=e.propsData,I=e.info,N=e.msg,b=e.stack;r.acData={type:this._options.storeVueErr,path:window.location.href,sTme:s().timeStamp,componentName:x,fileName:k,propsData:L,info:I,msg:N,err:b};break;case this._options.storeCodeErr:var O=e.msg,X=e.line,q=e.col,U=e.err;r.acData={type:this._options.storeCodeErr,path:window.location.href,sTme:s().timeStamp,msg:O,line:X,col:q,err:U};break;case this._options.storeSourceErr:var M=e.tagName,H=e.outerHTML,j=e.resourceUri,V=e.currentSrc;r.acData={type:this._options.storeSourceErr,path:window.location.href,sTme:s().timeStamp,fileName:V,resourceUri:j,tagName:M,outerHTML:H};break;case this._options.storePrmseErr:var J=e.reason;r.acData={type:this._options.storePrmseErr,path:window.location.href,sTme:s().timeStamp,reason:J};break;case this._options.storeCustom:var B=e.cusKey,W=e.cusVal;r.acData={type:this._options.storeCustom,path:window.location.href,sTme:s().timeStamp,cusKey:B,cusVal:W};break;case this._options.storeTiming:var F=e.WT,G=e.TCP,K=e.ONL,Q=e.ALLRT,z=e.TTFB,Y=e.DNS;r.acData={type:this._options.storeTiming,path:window.location.href,sTme:s().timeStamp,WT:F,TCP:G,ONL:K,ALLRT:Q,TTFB:z,DNS:Y};break;case this._options.storeCompErr:var Z=e.componentsTimes;r.acData={type:this._options.storeCompErr,path:window.location.href,sTme:s().timeStamp,componentsTimes:Z}}this._acData.push(r),this._options.openReducer?(a(this._options,this._options.cacheEventStorage,JSON.stringify(this._acData)),!this._options.manualReport&&this._options.sizeLimit&&this._acData.length>=this._options.sizeLimit&&(this._vue_&&this._vue_.$nextTick?this._vue_.$nextTick((function(){o.postAcData()})):this.postAcData())):this._vue_&&this._vue_.$nextTick?this._vue_.$nextTick((function(){o.postAcData()})):this.postAcData()},c.prototype.setCustomAc=function(t){var e=t.cusKey;void 0===e&&(e="custom");var o=t.cusVal;void 0===o&&(o=""),this._setAcData(this._options.storeCustom,{cusKey:e,cusVal:o})},c.prototype.postAcData=function(){if(!o(this._acData)&&0!==this._acData.length){var t,e,r,i=JSON.stringify(this._acData);this._options.useImgSend?(new Image).src=this._options.imageUrl+"?acError="+i:(void 0===(t={type:"POST",dataType:"json",contentType:"application/json",data:i,url:this._options.postUrl})&&(t={}),t.type=(t.type||"GET").toUpperCase(),t.dataType=t.dataType||"json",t.async=t.async||!0,t.data&&(r=t.data),window.XMLHttpRequest?(e=new XMLHttpRequest).overrideMimeType&&e.overrideMimeType("text/xml"):e=new ActiveXObject("Microsoft.XMLHTTP"),"GET"===t.type?(e.open("GET",t.url+"?"+r,t.async),e.send(null)):"POST"===t.type&&(e.open("POST",t.url,t.async),e.setRequestHeader("Content-Type","application/json; charset=UTF-8"),r?e.send(r):e.send())),this._acData=[],function(t,e){t.useStorage?window.localStorage.removeItem(e):a(t,e,"",-1)}(this._options,this._options.cacheEventStorage)}},c.prototype.setUserToken=function(t){this._userToken=t},c.install=function(t,e){return function t(e,o,r){t.installed||(t.installed=!0,e.mixin({watch:{$route:function(t,e){this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openPage&&this.$vueDataAc._mixinRouterWatch(t,e,!0)}},beforeCreate:function(){this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openComponent&&this.$vueDataAc._mixinComponentsPerformanceStart(this),this._uid===this.$root._uid&&this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openPage&&this.$vueDataAc._mixinRouterWatch(null,null,!1)},beforeDestroy:function(){this.$vueDataAc&&this.$vueDataAc.installed&&this._uid===this.$root._uid&&this.$vueDataAc&&this.$vueDataAc.postAcData()},mounted:function(){this.$vueDataAc&&this.$vueDataAc.installed&&(this.$vueDataAc._options.openInput&&(this.$vueDataAc._componentLoadCount++,this.$nextTick((function(){0==--this.$vueDataAc._componentLoadCount&&this.$vueDataAc._mixinInputEvent(this)}))),this.$vueDataAc._options.openComponent&&this.$nextTick((function(){this.$vueDataAc._mixinComponentsPerformanceEnd(this)})))},beforeUpdate:function(){this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openComponent&&this.$vueDataAc._mixinComponentsPerformanceStart(this)},updated:function(){this.$vueDataAc&&this.$vueDataAc.installed&&this.$vueDataAc._options.openComponent&&this.$nextTick((function(){this.$vueDataAc._mixinComponentsPerformanceEnd(this)}))}}),e.prototype.$vueDataAc=new r(o,e))}(t,e,c)},c.version="2.0.8",c},"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).VueDataAc=e(); -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * VueDataAc 配置 4 | * */ 5 | var OPTIONS = { 6 | useImgSend: false, 7 | selector: 'input,textArea', 8 | } 9 | Vue.use(VueDataAc, OPTIONS) 10 | 11 | /** 12 | * 文档目录 13 | * */ 14 | var menuData = window.__menuData__ || []; 15 | var subMenu = menuData.splice(0,1)[0]; 16 | subMenu.child = window.__docData__ 17 | /** 18 | * 默认实例 19 | * */ 20 | var app = new Vue({ 21 | el: '#app', 22 | data: { 23 | menuData: menuData, 24 | subMenu: subMenu, 25 | columns: [ 26 | {title: '公众号', key: 'a'}, 27 | {title: '打赏', key: 'b'} 28 | ], 29 | data: [], 30 | columns1:[ 31 | {title: '配置项', key: 'a'}, 32 | {title: '类型', key: 'b'}, 33 | {title: '默认值', key: 'c'}, 34 | {title: '是否可配置', key: 'd'}, 35 | {title: '说明', key: 'e'}, 36 | {title: '生效版本', key: 'f'} 37 | ], 38 | data1:[ 39 | { 40 | a: 'storeInput', 41 | b: 'String', 42 | c: '\'ACINPUT\'', 43 | d: '不建议', 44 | e: '输入框行为采集标识', 45 | f: '1.0.0' 46 | }, 47 | { 48 | a: 'storePage', 49 | b: 'String', 50 | c: '\'ACPAGE\'', 51 | d: '不建议', 52 | e: '页面访问信息采集标识', 53 | f: '1.0.0' 54 | }, 55 | { 56 | a: 'storeClick', 57 | b: 'String', 58 | c: '\'ACCLIK\'', 59 | d: '不建议', 60 | e: '点击事件采集标识', 61 | f: '1.0.0' 62 | }, 63 | { 64 | a: 'storeReqErr', 65 | b: 'String', 66 | c: '\'ACRERR\'', 67 | d: '不建议', 68 | e: '请求异常采集标识', 69 | f: '1.0.0' 70 | }, 71 | { 72 | a: 'storeTiming', 73 | b: 'String', 74 | c: '\'ACTIME\'', 75 | d: '不建议', 76 | e: '页面性能采集标识', 77 | f: '1.0.0' 78 | }, 79 | { 80 | a: 'storeCodeErr', 81 | b: 'String', 82 | c: '\'ACCERR\'', 83 | d: '不建议', 84 | e: '代码异常采集标识', 85 | f: '1.0.0' 86 | }, 87 | { 88 | a: 'storeCustom', 89 | b: 'String', 90 | c: '\'ACCUSTOM\'', 91 | d: '不建议', 92 | e: '自定义事件采集标识', 93 | f: '2.0.0' 94 | }, 95 | { 96 | a: 'storeSourceErr', 97 | b: 'String', 98 | c: '\'ACSCERR\'', 99 | d: '不建议', 100 | e: '资源加载异常采集标识', 101 | f: '2.0.0' 102 | }, 103 | { 104 | a: 'storePrmseErr', 105 | b: 'String', 106 | c: '\'ACPRERR\'', 107 | d: '不建议', 108 | e: 'promise抛出异常标识', 109 | f: '2.0.0' 110 | }, 111 | { 112 | a: 'storeCompErr', 113 | b: 'String', 114 | c: '\'ACCOMP\'', 115 | d: '不建议', 116 | e: 'Vue组件性能监控标识', 117 | f: '2.0.0' 118 | }, 119 | { 120 | a: 'storeVueErr', 121 | b: 'String', 122 | c: '\'ACVUERR\'', 123 | d: '不建议', 124 | e: 'Vue异常监控标识', 125 | f: '2.0.0' 126 | } 127 | ], 128 | columns2:[ 129 | {title: '配置项', key: 'a'}, 130 | {title: '类型', key: 'b'}, 131 | {title: '默认值', key: 'c'}, 132 | {title: '是否可配置', key: 'd'}, 133 | {title: '说明', key: 'e'}, 134 | {title: '生效版本', key: 'f'} 135 | ], 136 | data2:[ 137 | { 138 | a: 'userSha', 139 | b: 'String', 140 | c: '\'vue_ac_userSha\'', 141 | d: '可以', 142 | e: '用户标识存储在Storage中的key,有冲突可修改', 143 | f: '1.0.0' 144 | }, 145 | { 146 | a: 'useImgSend', 147 | b: 'Boolean', 148 | c: 'true', 149 | d: '可以', 150 | e: '是否使用图片上报数据, 设置为 false 为 xhr 接口请求上报', 151 | f: '2.0.0' 152 | }, 153 | { 154 | a: 'useStorage', 155 | b: 'Boolean', 156 | c: 'true', 157 | d: '可以', 158 | e: '是否使用storage作为存储载体, 设置为 false 时使用cookie', 159 | f: '2.0.0' 160 | }, 161 | { 162 | a: 'maxDays', 163 | b: 'Number', 164 | c: '365', 165 | d: '可以', 166 | e: '如果使用cookie作为存储载体,此项生效,配置cookie存储时间,默认一年', 167 | f: '2.0.0' 168 | }, 169 | { 170 | a: 'openInput', 171 | b: 'Boolean', 172 | c: 'true', 173 | d: '可以', 174 | e: '是否开启输入数据采集', 175 | f: '1.0.0' 176 | }, 177 | { 178 | a: 'openCodeErr', 179 | b: 'Boolean', 180 | c: 'true', 181 | d: '可以', 182 | e: '是否开启代码异常采集', 183 | f: '1.0.0' 184 | }, 185 | { 186 | a: 'openClick', 187 | b: 'Boolean', 188 | c: 'true', 189 | d: '可以', 190 | e: '是否开启点击数据采集', 191 | f: '1.0.0' 192 | }, 193 | { 194 | a: 'openXhrQuery', 195 | b: 'Boolean', 196 | c: 'true', 197 | d: '可以', 198 | e: '采集接口异常时是否采集请求参数params', 199 | f: '2.0.0' 200 | }, 201 | { 202 | a: 'openXhrHock', 203 | b: 'Boolean', 204 | c: 'true', 205 | d: '可以', 206 | e: '是否开启xhr异常采集', 207 | f: '1.0.0' 208 | }, 209 | { 210 | a: 'openPerformance', 211 | b: 'Boolean', 212 | c: 'true', 213 | d: '可以', 214 | e: '是否开启页面性能采集', 215 | f: '1.0.0' 216 | }, 217 | { 218 | a: 'openPage', 219 | b: 'Boolean', 220 | c: 'true', 221 | d: '可以', 222 | e: '是否开启页面访问信息采集(PV/UV) ', 223 | f: '2.0.0' 224 | }, 225 | { 226 | a: 'openVueErr', 227 | b: 'Boolean', 228 | c: 'true', 229 | d: '可以', 230 | e: '是否开启Vue异常监控', 231 | f: '2.0.0' 232 | }, 233 | { 234 | a: 'openSourceErr', 235 | b: 'Boolean', 236 | c: 'true', 237 | d: '可以', 238 | e: '是否开启资源加载异常采集', 239 | f: '2.0.0' 240 | }, 241 | { 242 | a: 'openPromiseErr', 243 | b: 'Boolean', 244 | c: 'true', 245 | d: '可以', 246 | e: '是否开启promise异常采集', 247 | f: '2.0.0' 248 | }, 249 | { 250 | a: 'openComponent', 251 | b: 'Boolean', 252 | c: 'true', 253 | d: '可以', 254 | e: '是否开启组件性能采集', 255 | f: '2.0.0' 256 | }, 257 | { 258 | a: 'maxComponentLoadTime', 259 | b: 'Number', 260 | c: '1000', 261 | d: '不建议改小', 262 | e: '组件渲染超时阈值,太小会导致信息过多,出发点是找出渲染异常的组件', 263 | f: '2.0.0' 264 | }, 265 | { 266 | a: 'openXhrTimeOut', 267 | b: 'Boolean', 268 | c: 'true', 269 | d: '可以', 270 | e: '是否开启请求超时上报', 271 | f: '2.0.0' 272 | }, 273 | { 274 | a: 'maxRequestTime', 275 | b: 'Number', 276 | c: '10000', 277 | d: '可以', 278 | e: '请求时间阈值,请求到响应大于此时间,会上报异常,openXhrTimeOut 为 false 时不生效', 279 | f: '2.0.0' 280 | }, 281 | { 282 | a: 'customXhrErrCode', 283 | b: 'String', 284 | c: '\'\'', 285 | d: '可以', 286 | e: '支持自定义响应code,当接口响应中的code为指定内容时上报异常', 287 | f: '2.0.0' 288 | } 289 | ], 290 | columns3:[ 291 | {title: '配置项', key: 'a'}, 292 | {title: '类型', key: 'b'}, 293 | {title: '默认值', key: 'c'}, 294 | {title: '采集范围', key: 'g'}, 295 | {title: '是否可配置', key: 'd'}, 296 | {title: '说明', key: 'e'}, 297 | {title: '生效版本', key: 'f'} 298 | ], 299 | data3:[ 300 | { 301 | a: 'selector', 302 | b: 'String', 303 | c: '\'input\'', 304 | g: '所有input输入框(全量采集)', 305 | d: '可以', 306 | e: '通过控制选择器来限定监听范围,使用document.querySelectorAll进行选择,值参考:https://www.runoob.com/cssref/css-selectors.html', 307 | f: '1.0.0' 308 | }, 309 | { 310 | a: 'selector', 311 | b: 'String', 312 | c: '\'input.isjs-ac\'', 313 | g: '所有class包含isjs-ac的input输入框(埋点采集)', 314 | d: '可以', 315 | e: '同上', 316 | f: '1.0.0' 317 | }, 318 | { 319 | a: 'ignoreInputType', 320 | b: 'Array', 321 | c: '[\'password\', \'file\']', 322 | g: 'type不是password和file的输入框', 323 | d: '可以', 324 | e: '---', 325 | f: '2.0.0' 326 | }, 327 | { 328 | a: 'ignoreInputType', 329 | b: 'Array', 330 | c: '[ ]', 331 | g: '所有输入框', 332 | d: '可以', 333 | e: '---', 334 | f: '2.0.0' 335 | }, 336 | { 337 | a: 'classTag', 338 | b: 'String', 339 | c: '\'\'', 340 | g: '所有可点击元素(全量采集)', 341 | d: '可以', 342 | e: '点击事件埋点标识, 自动埋点时请配置空字符串', 343 | f: '1.0.0' 344 | }, 345 | { 346 | a: 'classTag', 347 | b: 'String', 348 | c: '\'isjs-ac\'', 349 | g: '只会采集 class 包含 isjs-ac 元素的点击(埋点采集)', 350 | d: '可以', 351 | e: '点击事件埋点标识, 自动埋点时请配置空字符串', 352 | f: '1.0.0' 353 | }, 354 | { 355 | a: 'maxHelpfulCount', 356 | b: 'Number', 357 | c: '5', 358 | g: '全量采集场景下,为了使上报数据准确,我们会递归父元素,找到一个有class或id的祖先元素,此项配置递归次数', 359 | d: '不建议', 360 | e: '页面层次较深情况下,建议保留配置,以减少性能损耗', 361 | f: '2.0.0' 362 | } 363 | ], 364 | columns4:[ 365 | {title: '配置项', key: 'a'}, 366 | {title: '类型', key: 'b'}, 367 | {title: '默认值', key: 'c'}, 368 | {title: '是否可配置', key: 'd'}, 369 | {title: '说明', key: 'e'}, 370 | {title: '生效版本', key: 'f'} 371 | ], 372 | data4:[ 373 | { 374 | a: 'imageUrl', 375 | b: 'String', 376 | c: '\'https://data.ccedit.com/lib/image/ac.png\'', 377 | d: '可以', 378 | e: '《建议》 图片上报地址(通过1*1px图片接收上报信息)依赖 useImgSend 配置打开', 379 | f: '1.0.0' 380 | }, 381 | { 382 | a: 'postUrl', 383 | b: 'String', 384 | c: '\'https://data.ccedit.com/logStash/push\'', 385 | d: '可以', 386 | e: '接口上报地址', 387 | f: '1.0.0' 388 | }, 389 | { 390 | a: 'openReducer', 391 | b: 'Boolean', 392 | c: 'false', 393 | d: '可以', 394 | e: '是否开启节流,用于限制上报频率,开启后sizeLimit,manualReport生效', 395 | f: '2.0.0' 396 | }, 397 | { 398 | a: 'sizeLimit', 399 | b: 'Number', 400 | c: '20', 401 | d: '可以', 402 | e: '采集数据超过指定条目时自动上报,依赖 openReducer == true, 优先级:2 ', 403 | f: '2.0.0' 404 | }, 405 | { 406 | a: 'cacheEventStorage', 407 | b: 'String', 408 | c: '\'ac_cache_data\'', 409 | d: '可以', 410 | e: '开启节流后数据本地存储key', 411 | f: '2.0.0' 412 | }, 413 | { 414 | a: 'manualReport', 415 | b: 'Boolean', 416 | c: 'false', 417 | d: '可以', 418 | e: '强制手动上报,开启后只能调用postAcData方法上报,依赖 openReducer == true,优先级:1 ', 419 | f: '2.0.0' 420 | } 421 | ] 422 | }, 423 | computed: {}, 424 | watch: {}, 425 | methods: {}, 426 | components: {}, 427 | created: function(){ 428 | }, 429 | mounted: function(){ 430 | //控制loading层 431 | document.getElementById('app').style.display = 'block'; 432 | document.getElementById('load').style.display = 'none'; 433 | }, 434 | }) -------------------------------------------------------------------------------- /example/appoint/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * VueDataAc 配置 3 | * */ 4 | var OPTIONS = { 5 | useImgSend: false, 6 | selector: '.ac_input input, .ac_input textarea', //主动输入埋点 7 | classTag: 'ac_click', //主动点击埋点 8 | maxHelpfulCount: 6, //点击查找递归次数 9 | } 10 | Vue.use(VueDataAc, OPTIONS) 11 | 12 | /** 13 | * 默认实例 14 | * */ 15 | var app = new Vue({ 16 | el: '#app', 17 | data: { 18 | menuData: window.__menuData__ || [], 19 | inputValue: '', 20 | inputValue2: '', 21 | inputPassword: '', 22 | desc: '', 23 | desc1: '', 24 | }, 25 | computed: {}, 26 | watch: {}, 27 | methods: {}, 28 | components: {}, 29 | created: function(){ 30 | }, 31 | mounted: function(){ 32 | //控制loading层 33 | document.getElementById('app').style.display = 'block'; 34 | document.getElementById('load').style.display = 'none'; 35 | }, 36 | }) -------------------------------------------------------------------------------- /example/appoint/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue-dataAc 文档 9 | 10 | 11 | 20 | 21 | 22 |
23 | 82 |
83 |

加载中...

84 |
85 |