86 |
87 |
88 |
89 |
90 |
93 |
94 |
95 | {{item.text}}
96 |
97 |
98 |
99 |
100 |
103 |
104 |
105 | 输入框埋点采集
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | 说明:
133 |
134 | 主动埋点采集,需要对采集项目增加指定 class,并通过 selector 配置传递该 class 值
135 | 很多Ui库组件库都对原生元素做了封装,所以此处的 selector 根据实际场景进行配置
136 |
137 |
138 |
139 |
140 | 点击事件采集
141 |
142 |
143 |
144 | 已埋点按钮
145 | 未埋点按钮
146 | 未埋点按钮
147 | 未埋点按钮
148 |
149 |
150 | 已埋点icon
151 |
152 |
153 | 说明:
154 |
155 | 主动埋点采集,需要对指定的元素增加指定 class, 并配置对应的 classTag
156 | 要注意,主动埋点的情况下 maxHelpfulCount 同样生效,避免递归带来的性能问题
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/example/basic/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 | * vuerouter 测试数据
14 | * */
15 | var Home = { template: '
home page
' }
16 | var Login = { template: '
login page
' }
17 | var User = { template: '
user page
' }
18 |
19 | var routes = [
20 | { path: '/home', component: Home },
21 | { path: '/login', component: Login },
22 | { path: '/user', component: User },
23 | ]
24 | const router = new VueRouter({
25 | routes: routes
26 | })
27 | /**
28 | * 默认实例
29 | * */
30 | var app = new Vue({
31 | router,
32 | el: '#app',
33 | data: {
34 | menuData: window.__menuData__ || [],
35 | inputValue: '',
36 | inputPassword: '',
37 | desc: '',
38 | phone: 'apple',
39 | },
40 | computed: {},
41 | watch: {},
42 | methods: {
43 | /**
44 | * 自定义事件上报
45 | * */
46 | customEvent: function(){
47 | this.$vueDataAc.setCustomAc({
48 | cusKey: 'clickAnyButton',
49 | cusVal: 'click button ...'
50 | })
51 | }
52 | },
53 | components: {},
54 | created: function(){
55 | },
56 | mounted: function(){
57 | //控制loading层
58 | document.getElementById('app').style.display = 'block';
59 | document.getElementById('load').style.display = 'none';
60 | },
61 | })
--------------------------------------------------------------------------------
/example/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Vue-dataAc 文档
9 |
10 |
11 |
20 |
21 |
22 |
23 |
24 |
83 |
84 |
加载中...
85 |
86 |
87 |
88 |
89 |
90 |
91 |
94 |
95 |
96 | {{item.text}}
97 |
98 |
99 |
100 |
101 |
104 |
105 |
106 | 输入框信息采集
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | 说明:
129 |
130 | 连续输入时,会持续记录输入信息,失焦时上报数据
131 | 通过修改 openInput 打开/关闭 此功能
132 | 通过修改 selector 为 `input,textarea` 采集两种元素的输入信息
133 | 通过修改 ignoreInputType 对指定类型的输入框忽略采集,此处我们忽略了 password 类型的输入框
134 |
135 |
136 |
137 |
138 | 点击事件采集
139 |
140 |
141 |
142 | Default
143 | Primary
144 | Dashed
145 | Text
146 |
147 |
148 |
149 |
150 |
151 | 说明:
152 |
153 | 此处为全量采集示例
154 | 全量采集情况下页面上所有点击元素都可以采集
155 | 但是要注意,此处为了减少上报的脏数据,会从事件触发元素向上递归
156 | 直到找到一个有 class 或者 id 属性的元素,我们称之有效元素
157 | 最终上报该有效元素,找不到有效元素则不上报
158 | 向上递归停止的条件有两种,优先判断递归次数是否大于 maxHelpfulCount
159 | 其次判断是否递归到了 document 根元素
160 |
161 |
162 |
163 |
164 | 自定义事件采集
165 |
166 |
167 |
168 | 点击上报自定义事件
169 |
170 |
171 | 说明:
172 |
173 | 某些业务场景下,用户的操作来自于非主动操作
174 | 例如当用户处于某个状态,处于某个业务节点,查看到了某个内容,接口请求处于某种异常等等
175 | 支持在这些自定义场景下调用 setCustomAc 方法进行自定义上报,可以自定义上报 key 和 value
176 |
177 |
178 |
179 |
180 |
181 | 路由切换采集
182 |
183 |
184 |
185 | home
186 | login
187 | user
188 |
189 |
190 |
191 |
192 |
193 | 说明:
194 |
195 | 路由切换采集生效在使用 VueRouter 的前提下,因为我们监控 $route 的变化
196 | 但是当 url 发生变换的场景下,从 A 页面跳转到相同域的 B 页面,我们同样也有一套监控生效
197 | 所以哪怕你的项目里没有使用 VueRouter 是多页面的场景,也是可以采集到页面访问(ACPAGE)信息的
198 | 你可以点击左侧不同 demo 链接查看另一种数据
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
--------------------------------------------------------------------------------
/example/error/app.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * VueDataAc 配置
4 | * */
5 | var OPTIONS = {
6 | useImgSend: false,
7 | openClick: false
8 | }
9 | Vue.use(VueDataAc, OPTIONS)
10 |
11 |
12 | /**
13 | * 默认实例
14 | * */
15 | var app = new Vue({
16 | el: '#app',
17 | data: {
18 | menuData: window.__menuData__ || []
19 | },
20 | computed: {},
21 | watch: {},
22 | methods: {
23 | promiseErr: function(){
24 | new Promise(function (resolve,reject) {
25 | reject();
26 | })
27 | },
28 | sendError: function(){
29 | axios.get('/error/log')
30 | .then(function (response) {
31 | console.log(response);
32 | })
33 | .catch(function (error) {
34 | console.log(error);
35 | });
36 | }
37 | },
38 | components: {},
39 | created: function(){},
40 | mounted: function(){
41 | //控制loading层
42 | document.getElementById('app').style.display = 'block';
43 | document.getElementById('load').style.display = 'none';
44 | },
45 | })
46 |
--------------------------------------------------------------------------------
/example/error/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Vue-dataAc 文档
9 |
10 |
11 |
20 |
21 |
22 |
23 |
24 |
83 |
84 |
加载中...
85 |
86 |
87 |
88 |
89 |
90 |
91 |
94 |
95 |
96 | {{item.text}}
97 |
98 |
99 |
100 |
101 |
104 |
105 |
106 | 代码异常采集
107 |
108 |
109 |
110 | 点击按钮执行语法错误代码
111 |
112 |
113 | 说明:
114 |
115 | 监控代码异常,尽可能在项目中启用 eslint 语法检查,避免简单错误上线
116 | vue已经代理了大部分异常,所以此处上报的是Vue异常
117 |
118 |
119 |
120 |
121 |
122 | 请求异常
123 |
124 |
125 |
126 | 点击按钮发送请求
127 |
128 |
129 | 说明:
130 |
131 | 点击按钮会通过 axios 请求一个不存在的地址
132 |
133 |
134 |
135 |
136 |
137 | promise 异常
138 |
139 |
140 |
141 | 点击按钮执行 promise 异常代码
142 |
143 |
144 | 说明:
145 |
146 | promise 中的 reject() 没有被正常处理时抛出异常
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/example/imgreport/app.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * VueDataAc 配置
4 | * */
5 | var OPTIONS = {
6 | imageUrl: 'https://data.ccedit.com/lib/image/ac.png',
7 | openClick: false
8 | }
9 | Vue.use(VueDataAc, OPTIONS)
10 |
11 |
12 | /**
13 | * vuerouter 测试数据
14 | * */
15 | var Home = { template: '
home page
' }
16 | var Login = { template: '
login page
' }
17 | var User = { template: '
user page
' }
18 |
19 | var routes = [
20 | { path: '/home', component: Home },
21 | { path: '/login', component: Login },
22 | { path: '/user', component: User },
23 | ]
24 | const router = new VueRouter({
25 | routes: routes
26 | })
27 | /**
28 | * 默认实例
29 | * */
30 | var app = new Vue({
31 | router,
32 | el: '#app',
33 | data: {
34 | menuData: window.__menuData__ || [],
35 | inputValue: '',
36 | inputPassword: '',
37 | desc: '',
38 | phone: 'apple',
39 | },
40 | computed: {},
41 | watch: {},
42 | methods: {
43 | /**
44 | * 自定义事件上报
45 | * */
46 | customEvent: function(){
47 | this.$vueDataAc.setCustomAc({
48 | cusKey: 'clickAnyButton',
49 | cusVal: 'click button ...'
50 | })
51 | }
52 | },
53 | components: {},
54 | created: function(){
55 | },
56 | mounted: function(){
57 | //控制loading层
58 | document.getElementById('app').style.display = 'block';
59 | document.getElementById('load').style.display = 'none';
60 | },
61 | })
--------------------------------------------------------------------------------
/example/imgreport/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Vue-dataAc 文档
9 |
10 |
11 |
20 |
21 |
22 |
23 |
82 |
83 |
加载中...
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 |
94 |
95 | {{item.text}}
96 |
97 |
98 |
99 |
100 |
103 |
104 |
105 | 自定义事件采集
106 |
107 |
108 |
109 | 点击上报自定义事件
110 |
111 |
112 | 说明:
113 |
114 | 此页面中我们使用图片进行上报,这样做的好处是不占用接口请求数量
115 | 另一个好处是可以将 nginx 日志和公司 elk 进行对接,实现实时告警推送
116 | 当然,http 2.0时代不存在请求数占用的问题,所以此处根据实际场景进行选择即可
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Vue-dataAc 文档
9 |
10 |
11 |
20 |
21 |
22 |
23 |
24 |
83 |
84 |
加载中...
85 |
86 |
87 |
88 |
89 |
90 |
91 |
94 |
95 |
96 |
97 | {{subMenu.text}}
98 |
99 |
100 |
101 |
102 | {{item.text}}
103 |
104 |
105 |
106 |
107 | {{item.text}}
108 |
109 |
110 |
111 | {{subitem.text}}
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {{item.text}}
120 |
121 |
122 |
123 |
124 |
127 |
128 |
129 | 支持一下
130 |
131 | GitHub:Vue-dataAc
132 | 后台:Vue-dataAc-server
133 | 说明:
134 |
135 | 开发不易,希望大家能帮忙点个star ♪(・ω・)ノ
136 |
137 |
138 |
139 |
140 | 标识类配置,作为数据上报信息的分类标识
141 |
142 |
143 | 说明:
144 |
145 | 数据上报到后台,用上面的标记进行筛选分类,需要做通知告警的发送消息
146 |
147 |
148 |
149 |
150 | 全局开关,用来自定义采集范围
151 |
152 |
153 | 说明:
154 |
155 | 以上开关默认都是打开状态,这就导致上报数据量过大,过于频繁
156 | 所以根据实际需要,不要要用户行为,不需要性能分析就关闭开关
157 | 上面的配置项有几组关联,分别是:
158 | useStorage,maxDays
159 | openComponent,maxComponentLoadTime
160 | openXhrTimeOut,maxRequestTime,customXhrErrCode,openXhrHock
161 |
162 |
163 |
164 |
165 | 行为采集配置
166 |
167 |
168 | 说明:
169 |
170 | 行为数据会采集用户点击输入等行为,用于轨迹追踪,防抵赖,转化分析等场景
171 | 根据实际使用情况进行搜集
172 | 这里面埋点和全量采集的区别我们放到了class上面,通过指定要采集的元素,精准定位数据
173 | 要注意的一点是,点击事件触发的元素可能不是目标元素,我们通过向上递归查找有效元素(有class或id)
174 |
175 |
176 |
177 |
178 | 数据上报配置
179 |
180 |
181 | 说明:
182 |
183 | 行为数据会采集用户点击输入等行为,用于轨迹追踪,防抵赖,转化分析等场景
184 | 根据实际使用情况进行搜集
185 | 这里面埋点和全量采集的区别我们放到了class上面,通过指定要采集的元素,精准定位数据
186 | 要注意的一点是,点击事件触发的元素可能不是目标元素,我们通过向上递归查找有效元素(有class或id)
187 |
188 |
189 |
190 |
191 | vue.$vueDataAc.setCustomAc( {cusKey: String, cusVal: Any} )
192 |
193 |
194 | 用于自定义事件的约定上报,例如在业务场景中对某些逻辑的埋点
195 | 自定义事件上报逻辑与其他事件上报共用,可以通过 openReducer 限制频率
196 |
197 |
198 |
199 |
200 | vue.$vueDataAc.postAcData()
201 |
202 |
203 | 手动上报当前采集信息
204 |
205 |
206 |
207 |
208 | vue.$vueDataAc.setUserToken(userToken: String)
209 |
210 |
211 | 用于关联用户后台标记,利用用户登录后的userid,sessionId
212 | 目的是将前后台日志打通,方便查找模拟用户
213 |
214 |
215 |
216 |
217 | 页面访问,路由跳转,等同于PV/UV数据
218 |
219 |
220 | {
221 | "uuid": "F6A6C801B7197603", //用户标识
222 | "t" : "", //后端 用户标识/登录标识 默认为空,
223 | 通过setUserToken设置
224 | "acData" : {
225 | "type" : "ACPAGE" //行为标识
226 | "sTme" : 1591760011268 //数据上报时间
227 | "fromPath" : "/register?type=1" //来源路由
228 | "formParams" : "{'type': 1}" //来源参数
229 | "toPath" : "/login" //目标路由
230 | "toParams" : "{}" //目标参数
231 | "inTime" : 1591760011268 //页面进入时间
232 | "outTime" : 1591760073422 //离开页面时间
233 | }
234 | }
235 |
236 |
237 |
238 |
239 | 代码异常数据
240 |
241 |
242 | {
243 | "uuid": "F6A6C801B7197603", //用户标识
244 | "t" : "",
245 | "acData" : {
246 | "type" : "ACCERR", //上报数据类型:代码异常数据
247 | "path" : "www.domain.com/w/w/w/", //事件发生页面的url
248 | "sTme" : "1591760073422", //事件上报时间
249 | "msg" : "script error", //异常摘要
250 | "line" : "301", //代码行数
251 | "col" : "13", //代码列下标
252 | "err" : "error message", //错误信息
253 | }
254 | }
255 |
256 |
257 |
258 |
259 | 资源加载异常数据
260 |
261 |
262 | {
263 | "uuid": "F6A6C801B7197603", //用户标识
264 | "t" : "",
265 | "acData" : {
266 | "type" : "ACSCERR", //上报数据类型:资源加载异常数据
267 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
268 | "sTme" : "1591760073422", //事件上报时间
269 | "fileName" : "test.js", //文件名
270 | "resourceUri" : "http://ccedit.com/js/test.js", //资源地址
271 | "tagName" : "script", //标签类型
272 | "outerHTML" : "script ...", //标签内容
273 | }
274 | }
275 |
276 |
277 |
278 |
279 | Promise 异常数据
280 |
281 |
282 | {
283 | "uuid": "F6A6C801B7197603", //用户标识
284 | "t" : "",
285 | "acData" : {
286 | "type" : "ACPRERR", //上报数据类型:Promise 异常数据
287 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
288 | "sTme" : "1591760073422", //事件上报时间
289 | "reason" : "reason" //异常说明
290 | }
291 | }
292 |
293 |
294 |
295 |
296 | 自定义事件数据
297 |
298 |
299 | {
300 | "uuid": "F6A6C801B7197603", //用户标识
301 | "t" : "",
302 | "acData" : {
303 | "type" : "ACCUSTOM", //上报数据类型:自定义事件数据
304 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
305 | "sTme" : "1591760073422", //事件上报时间
306 | "cusKey" : "click-button-001" //自定义事件key,用户定义
307 | "cusVal" :"1" //自定义事件值,用户定义
308 | }
309 | }
310 |
311 |
312 |
313 |
314 | Vue异常监控数据
315 |
316 |
317 | {
318 | "uuid": "F6A6C801B7197603", //用户标识
319 | "t" : "",
320 | "acData" : {
321 | "type" : "ACVUERR", //上报数据类型:Vue异常监控数据
322 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
323 | "sTme" : "1591760073422", //事件上报时间
324 | "componentName" : "Button" //组件名
325 | "fileName" : "button.js" //组件文件
326 | "propsData" : "{}" //组件props
327 | "err" : "..." //错误堆栈
328 | "info" : "信息" //错误信息
329 | "msg" : "1" //异常摘要
330 | }
331 | }
332 |
333 |
334 |
335 |
336 | 点击事件监控数据
337 |
338 |
339 | {
340 | "uuid": "F6A6C801B7197603", //用户标识
341 | "t" : "",
342 | "acData" : {
343 | "type" : "ACCLIK", //上报数据类型:点击事件监控数据
344 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
345 | "sTme" : "1591760073422", //事件上报时间
346 | "eId" : "" //元素id属性
347 | "className" : "login-form" //点击元素class属性
348 | "val" : "标题" //元素value或者innertext
349 | "attrs" : "{class:'...', placeholder:'...', type:'...'}" //元素所有属性对象
350 | }
351 | }
352 |
353 |
354 |
355 |
356 | 输入事件监控数据
357 |
358 |
359 | {
360 | "uuid": "F6A6C801B7197603", //用户标识
361 | "t" : "",
362 | "acData" : {
363 | "type" : "ACINPUT", //上报数据类型:输入事件监控数据
364 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
365 | "sTme" : "1591760073422", //事件上报时间
366 | "eId" : "" //元素id属性
367 | "className" : "van-field__control" //元素class属性
368 | "val" : "0:111,638:11,395:1,327:,1742:5,214:55,207:555,175:5555"
369 | //时间:当前值,用逗号分隔,体现时间变化
370 | "attrs" : "{class:'...', placeholder:'...', type:'...'}"
371 | //元素所有属性对象
372 | }
373 | }
374 |
375 |
376 |
377 |
378 | 接口异常数据(包含 请求时间过长/自定义code/请求错误)
379 |
380 |
381 | {
382 | "uuid": "F6A6C801B7197603", //用户标识
383 | "t" : "",
384 | "acData" : {
385 | "type" : "ACRERR", //上报数据类型:接口异常数据
386 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
387 | "sTme" : "1591760073422", //事件上报时间
388 | "errSubType" : "http/time/custom" //异常类型:【time: 请求时间过长】【custom: 自定义code】【http:请求错误】
389 | "responseURL" : "/static/push" //请求接口
390 | "method" : "GET" //请求方式
391 | "readyState" : 4 //xhr.readyState状态码
392 | "status" : "404" //请求状态码
393 | "statusText" : "not found" //错误描述
394 | "requestTime" : 3000 //请求耗时
395 | "response" : "{...}" //接口响应摘要,截取前100个字符
396 | "query" : "{}" //请求参数,用 openXhrQuery 配置打开,注意用户信息泄露
397 | }
398 | }
399 |
400 |
401 |
402 |
403 | 页面性能监控数据
404 |
405 |
406 | {
407 | "uuid": "F6A6C801B7197603", //用户标识
408 | "t" : "",
409 | "acData" : {
410 | "type" : "ACRERR", //上报数据类型:页面性能监控数据
411 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
412 | "sTme" : "1591760073422", //事件上报时间
413 | "WT" : 1000 //白屏时间
414 | "TCP" : 1000 //TCP连接耗时
415 | "ONL" : 1000 //执行onload事件耗时
416 | "ALLRT" : 1000 //所有请求耗时
417 | "TTFB" : 1000 //TTFB读取页面第一个字节的时间
418 | "DNS" : 1000 //DNS查询时间
419 | }
420 | }
421 |
422 |
423 |
424 |
425 | Vue组件渲染异常数据
426 |
427 |
428 | {
429 | "uuid": "F6A6C801B7197603", //用户标识
430 | "t" : "",
431 | "acData" : {
432 | "type" : "ACCOMP", //上报数据类型:页面性能监控
433 | "path" : "www.domain.com/w/w/w/", //事件发生页面地址
434 | "sTme" : "1591760073422", //事件上报时间
435 | "componentsTimes" : [ //渲染超时组件列表
436 | '组件名': [1000,1200,1090]
437 | ]
438 | }
439 | }
440 |
441 |
442 |
443 |
444 | 解决方案
445 |
446 | 1.我需要采集用户行为吗?
447 | -- 用户行为相关数据,我认为对产品有益,可以用于分析转化,页面热点图等
448 | -- 根据数据对产品进行调整。所以看你的产品类型 2C 的产品一般有这样的需求
449 |
450 | 2.我需要监控页面异常吗?
451 | -- 从前端角度是有必要的,用户遇到问题,经过问题上报,汇总,最终分配到你,时间不可控
452 | -- 能在第一时间对端上的问题进行告警,会大大提高解决问题的效率
453 | -- 所以我认为需要有一个监控系统作为生产安全的兜底方案
454 |
455 | 3.我需要采集页面性能&组件性能吗??
456 | -- 页面性能组件性能,我建议开启,生产测试环境可能因为数据不相同,会有差异性bug
457 | -- 可能会导致组件渲染慢,影响体验
458 | -- 但是前提是要把阈值调大,以免数据过多,大量数据上报,会降低对报警的敏感度
459 |
460 | 4.数据采集后如何进行整理分析?
461 | -- 我们的数据上报分为两种,接口和图片
462 | -- 其实不论哪一种方式,最终都要将数据本地化,持久化。
463 | -- 可以问一下公司后端的同事,他们的数据怎么分析,对接他们的上报接口就可以。
464 | -- 一般这样的日志搜集分析,会用到 ELK 系统。没有的话让运维帮忙搭建一套。
465 | -- 通过接口将上报的数据存储到本地文件或数据库中。或是通过图片上报,将数据存储在nginx中
466 | -- 然后用ELK对接日志即可。ELK有提供查询API,你可以做一套轮训告警系统
467 |
468 | 5.我需要采集用户行为吗?
469 | -- 前端日志的唯一标识是uuid,后端唯一标识可以通过 setUserToken方法将用户唯一 id 和 uuid 做关联
470 |
471 | 6.我需要采集用户行为吗?
472 | -- 建议你分批次,分功能,做足够量的测试之后,逐步打开开关上线。
473 | -- 我只能保证在我的场景下可以正常使用,但是不同的产品,不同的用户场景,不能百分百保证
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
--------------------------------------------------------------------------------
/example/lib/axios.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new s(e),n=i(s.prototype.request,t);return o.extend(n,s.prototype,t),o.extend(n,t),n}var o=n(2),i=n(3),s=n(4),a=n(22),u=n(10),c=r(u);c.Axios=s,c.create=function(e){return r(a(c.defaults,e))},c.Cancel=n(23),c.CancelToken=n(24),c.isCancel=n(9),c.all=function(e){return Promise.all(e)},c.spread=n(25),e.exports=c,e.exports.default=c},function(e,t,n){"use strict";function r(e){return"[object Array]"===j.call(e)}function o(e){return"undefined"==typeof e}function i(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function s(e){return"[object ArrayBuffer]"===j.call(e)}function a(e){return"undefined"!=typeof FormData&&e instanceof FormData}function u(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function f(e){return"number"==typeof e}function p(e){return null!==e&&"object"==typeof e}function d(e){return"[object Date]"===j.call(e)}function l(e){return"[object File]"===j.call(e)}function h(e){return"[object Blob]"===j.call(e)}function m(e){return"[object Function]"===j.call(e)}function y(e){return p(e)&&m(e.pipe)}function g(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function v(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function x(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function w(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n
=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},i.forEach(["delete","get","head"],function(e){u.headers[e]={}}),i.forEach(["post","put","patch"],function(e){u.headers[e]=i.merge(a)}),e.exports=u},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(13),i=n(5),s=n(16),a=n(19),u=n(20),c=n(14);e.exports=function(e){return new Promise(function(t,f){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password||"";d.Authorization="Basic "+btoa(h+":"+m)}var y=s(e.baseURL,e.url);if(l.open(e.method.toUpperCase(),i(y,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l.onreadystatechange=function(){if(l&&4===l.readyState&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var n="getAllResponseHeaders"in l?a(l.getAllResponseHeaders()):null,r=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:r,status:l.status,statusText:l.statusText,headers:n,config:e,request:l};o(t,f,i),l=null}},l.onabort=function(){l&&(f(c("Request aborted",e,"ECONNABORTED",l)),l=null)},l.onerror=function(){f(c("Network Error",e,null,l)),l=null},l.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),f(c(t,e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=n(21),v=(e.withCredentials||u(y))&&e.xsrfCookieName?g.read(e.xsrfCookieName):void 0;v&&(d[e.xsrfHeaderName]=v)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),r.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),f(e),l=null)}),void 0===p&&(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(14);e.exports=function(e,t,n){var o=n.config.validateStatus;!o||o(n.status)?e(n):t(r("Request failed with status code "+n.status,n.config,null,n.request,n))}},function(e,t,n){"use strict";var r=n(15);e.exports=function(e,t,n,o,i){var s=new Error(e);return r(s,t,n,o,i)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},function(e,t,n){"use strict";var r=n(17),o=n(18);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,i,s={};return e?(r.forEach(e.split("\n"),function(e){if(i=e.indexOf(":"),t=r.trim(e.substr(0,i)).toLowerCase(),n=r.trim(e.substr(i+1)),t){if(s[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?s[t]=(s[t]?s[t]:[]).concat([n]):s[t]=s[t]?s[t]+", "+n:n}}),s):s}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,i,s){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(i)&&a.push("domain="+i),s===!0&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){t=t||{};var n={},o=["url","method","params","data"],i=["headers","auth","proxy"],s=["baseURL","url","transformRequest","transformResponse","paramsSerializer","timeout","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","maxContentLength","validateStatus","maxRedirects","httpAgent","httpsAgent","cancelToken","socketPath"];r.forEach(o,function(e){"undefined"!=typeof t[e]&&(n[e]=t[e])}),r.forEach(i,function(o){r.isObject(t[o])?n[o]=r.deepMerge(e[o],t[o]):"undefined"!=typeof t[o]?n[o]=t[o]:r.isObject(e[o])?n[o]=r.deepMerge(e[o]):"undefined"!=typeof e[o]&&(n[o]=e[o])}),r.forEach(s,function(r){"undefined"!=typeof t[r]?n[r]=t[r]:"undefined"!=typeof e[r]&&(n[r]=e[r])});var a=o.concat(i).concat(s),u=Object.keys(t).filter(function(e){return a.indexOf(e)===-1});return r.forEach(u,function(r){"undefined"!=typeof t[r]?n[r]=t[r]:"undefined"!=typeof e[r]&&(n[r]=e[r])}),n}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])});
--------------------------------------------------------------------------------
/example/lib/image/ac.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Li-vien/Vue-dataAc/d7c2a0a03b240212409762b62ba833057b529ff9/example/lib/image/ac.png
--------------------------------------------------------------------------------
/example/lib/menu.js:
--------------------------------------------------------------------------------
1 | window.__menuData__ = [
2 | {
3 | name: '0',
4 | icon: 'ios-document-outline',
5 | text: '文档',
6 | path: '/index.html'
7 | },
8 | {
9 | name: '9',
10 | icon: 'ios-easel-outline',
11 | text: '后台数据展示',
12 | path: '/log/index.html'
13 | },
14 | {
15 | name: '1',
16 | icon: 'ios-body-outline',
17 | text: '行为监控Demo',
18 | path: '/basic/index.html'
19 | },
20 | {
21 | name: '2',
22 | icon: 'ios-bug-outline',
23 | text: '异常监控Demo',
24 | path: '/error/index.html'
25 | },
26 | {
27 | name: '3',
28 | icon: 'ios-pulse-outline',
29 | text: '性能监控Demo',
30 | path: '/performance/index.html'
31 | },
32 | {
33 | name: '4',
34 | icon: 'ios-pin-outline',
35 | text: '主动埋点Demo',
36 | path: '/appoint/index.html'
37 | },
38 | {
39 | name: '5',
40 | icon: 'ios-send-outline',
41 | text: '图片数据上报Demo',
42 | path: '/imgreport/index.html'
43 | },
44 | {
45 | name: '6',
46 | icon: 'ios-send-outline',
47 | text: '上报节流Demo',
48 | path: '/reportsize/index.html'
49 | },
50 | {
51 | name: '8',
52 | icon: 'ios-send-outline',
53 | text: '关联登录信息Demo',
54 | path: '/token/index.html'
55 | }
56 | ]
57 | window.__docData__ = [
58 | {
59 | name: '0-0',
60 | icon: 'logo-github',
61 | text: '支持一下',
62 | path: '#github'
63 | },
64 | {
65 | name: '0-1',
66 | icon: 'md-list-box',
67 | text: '标识类配置',
68 | path: '#tag'
69 | },
70 | {
71 | name: '0-2',
72 | icon: 'md-list-box',
73 | text: '全局开关',
74 | path: '#switch'
75 | },
76 | {
77 | name: '0-3',
78 | icon: 'md-list-box',
79 | text: '行为采集',
80 | path: '#behavior'
81 | },
82 | {
83 | name: '0-4',
84 | icon: 'md-list-box',
85 | text: '数据上报配置',
86 | path: '#datareport'
87 | },
88 | {
89 | name: '0-5',
90 | icon: 'md-list-box',
91 | text: 'setCustomAc 方法',
92 | path: '#setCustomAc'
93 | },
94 | {
95 | name: '0-6',
96 | icon: 'md-list-box',
97 | text: 'postAcData 方法',
98 | path: '#postAcData'
99 | },
100 | {
101 | name: '0-7',
102 | icon: 'md-list-box',
103 | text: 'setUserToken 方法',
104 | path: '#setUserToken'
105 | },
106 | {
107 | name: '0-8',
108 | icon: 'md-list-box',
109 | text: '上报数据示例',
110 | path: '#dataSource',
111 | child: [
112 | {
113 | name: '0-8-1',
114 | icon: 'md-list-box',
115 | text: '页面访问',
116 | path: '#dataSource1'
117 | },
118 | {
119 | name: '0-8-2',
120 | icon: 'md-list-box',
121 | text: '代码异常数据',
122 | path: '#dataSource2'
123 | },
124 | {
125 | name: '0-8-3',
126 | icon: 'md-list-box',
127 | text: '资源加载异常数据',
128 | path: '#dataSource3'
129 | },
130 | {
131 | name: '0-8-4',
132 | icon: 'md-list-box',
133 | text: 'Promise 异常数据',
134 | path: '#dataSource4'
135 | },
136 | {
137 | name: '0-8-5',
138 | icon: 'md-list-box',
139 | text: '自定义事件数据',
140 | path: '#dataSource5'
141 | },
142 | {
143 | name: '0-8-6',
144 | icon: 'md-list-box',
145 | text: 'Vue异常监控数据',
146 | path: '#dataSource6'
147 | },
148 | {
149 | name: '0-8-7',
150 | icon: 'md-list-box',
151 | text: '点击事件监控数据',
152 | path: '#dataSource7'
153 | },
154 | {
155 | name: '0-8-8',
156 | icon: 'md-list-box',
157 | text: '输入事件监控数据',
158 | path: '#dataSource8'
159 | },
160 | {
161 | name: '0-8-9',
162 | icon: 'md-list-box',
163 | text: '接口异常数据',
164 | path: '#dataSource9'
165 | },
166 | {
167 | name: '0-8-10',
168 | icon: 'md-list-box',
169 | text: '页面性能监控数据',
170 | path: '#dataSource10'
171 | },
172 | {
173 | name: '0-8-11',
174 | icon: 'md-list-box',
175 | text: 'Vue组件渲染异常数据',
176 | path: '#dataSource11'
177 | }
178 | ]
179 | },
180 | {
181 | name: '0-9',
182 | icon: 'ios-chatbubbles-outline',
183 | text: '解决方案',
184 | path: '#QA'
185 | }
186 | ]
--------------------------------------------------------------------------------
/example/lib/styles/basic.css:
--------------------------------------------------------------------------------
1 | *{
2 | -webkit-touch-callout:none; /*系统默认菜单被禁用*/
3 | -webkit-user-select:none; /*webkit浏览器*/
4 | -khtml-user-select:none; /*早期浏览器*/
5 | -moz-user-select:none;/*火狐*/
6 | -ms-user-select:none; /*IE10*/
7 | user-select:none;
8 | }
9 | .cancopy{
10 | -webkit-user-select:auto; /*webkit浏览器*/
11 | -khtml-user-select:auto; /*早期浏览器*/
12 | -moz-user-select:auto;/*火狐*/
13 | -ms-user-select:auto; /*IE10*/
14 | user-select:auto;
15 | }
16 | input,textarea {
17 | -webkit-user-select: auto; /*webkit浏览器*/
18 | }
19 | html, body {
20 | width: 100%;
21 | font-family: 'microsoft yahei','PingFang SC','STHeitiSC-Light','Helvetica',Arial,sans-serif;
22 | }
23 |
24 | body {
25 | overflow-x: hidden;
26 | -webkit-text-size-adjust: none;
27 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
28 | color: #232930;
29 | }
30 |
31 | html, body, div, span, applet, object, iframe,
32 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
33 | a, abbr, acronym, address, big, cite, code,
34 | del, dfn, em, img, ins, kbd, q, s, samp,
35 | small, strike, strong, sub, sup, tt, var,
36 | b, u, i, center,
37 | dl, dt, dd, ol, ul, li,
38 | fieldset, form, label, legend,
39 | table, caption, tbody, tfoot, thead, tr, th, td,
40 | article, aside, canvas, details, embed,
41 | figure, figcaption, footer, header,
42 | menu, nav, output, ruby, section, summary,
43 | time, mark, audio, video, input {
44 | box-sizing: border-box;
45 | margin: 0;
46 | padding: 0;
47 | border: 0;
48 | font-size: 100%;
49 | font-weight: normal;
50 | vertical-align: baseline;
51 | }
52 |
53 | /* HTML5 display-role reset for older browsers */
54 | article, aside, details, figcaption, figure,
55 | footer, header, menu, nav, section {
56 | display: block;
57 | }
58 |
59 | body {
60 | line-height: 1;
61 | }
62 |
63 | blockquote, q {
64 | quotes: none;
65 | }
66 |
67 | blockquote:before, blockquote:after,
68 | q:before, q:after {
69 | content: none;
70 | }
71 |
72 | table {
73 | border-collapse: collapse;
74 | border-spacing: 0;
75 | }
76 |
77 | /* custom */
78 | a {
79 | color: inherit;
80 | text-decoration: none;
81 | -webkit-backface-visibility: hidden;
82 | }
83 |
84 | li {
85 | list-style: none;
86 | }
87 |
88 | button {
89 | outline: none;
90 | border: 0;
91 | }
92 |
93 | input[type=text]::placeholder,
94 | input[type=number]::placeholder,
95 | input[type=tel]::placeholder {
96 | color: #BBB;
97 | }
98 |
99 | input {
100 | appearance: none;
101 | }
102 |
103 | .layout-con{
104 | height: 100%;
105 | width: 100%;
106 | }
107 | .menu-item span{
108 | display: inline-block;
109 | overflow: hidden;
110 | width: 69px;
111 | text-overflow: ellipsis;
112 | white-space: nowrap;
113 | vertical-align: bottom;
114 | transition: width .2s ease .2s;
115 | }
116 | .menu-item i{
117 | transform: translateX(0px);
118 | transition: font-size .2s ease, transform .2s ease;
119 | vertical-align: middle;
120 | font-size: 16px;
121 | }
122 | .collapsed-menu span{
123 | width: 0px;
124 | transition: width .2s ease;
125 | }
126 | .collapsed-menu i{
127 | transform: translateX(5px);
128 | transition: font-size .2s ease .2s, transform .2s ease .2s;
129 | vertical-align: middle;
130 | font-size: 22px;
131 | }
132 | .dataac-headLayout{
133 | max-height: 100vh;
134 | height: 100vh;
135 | box-sizing: border-box;
136 | }
137 | .dataac-sider{
138 | max-height: 100vh;
139 | height: 100vh;
140 | overflow: scroll;
141 | padding-bottom: 20px;
142 | }
143 | .dataAc-header{
144 | height: 50px;
145 | text-align: center;
146 | background: rgb(255, 255, 255);
147 | box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 3px 2px;
148 | position: absolute;
149 | left: 200px;
150 | right: 0;
151 | z-index: 10;
152 | }
153 | .dataAc-document{
154 | padding: 10px;
155 | margin-top: 60px;
156 | box-sizing: border-box;
157 | overflow: scroll;
158 | border-radius: 10px;
159 | background: rgb(255, 255, 255);
160 | }
161 | .dataAc-content{
162 | padding: 10px;
163 | box-sizing: border-box;
164 | margin: 66px 16px 16px;
165 | overflow: scroll;
166 | border-radius: 10px;
167 | background: rgb(255, 255, 255);
168 | }
169 | .dataAc-layout{
170 | height: 100vh;
171 | overflow: hidden;
172 | }
173 | .dataac-menuLogo{
174 | height: 60px;
175 | font-size: 16px;
176 | font-weight: bold;
177 | color: #eee;
178 | text-align: center;
179 | line-height: 60px;
180 | }
181 | .ivu-col {
182 | margin-bottom: 10px;
183 | }
184 | #app .ivu-card{
185 | margin-bottom: 15px;
186 | background-color: #f6f6f6;
187 | }
188 | #app .ivu-card .dataac-info{
189 | background-color: #2db7f5;
190 | color: #fff;
191 | padding: 10px;
192 | text-align: center;
193 | }
194 | #app .ivu-card .dataac-warn{
195 | background-color: #ff9900;
196 | color: #fff;
197 | padding: 10px;
198 | text-align: center;
199 | }
200 | #app .ivu-card .dataac-err{
201 | background-color: #ed4014;
202 | color: #fff;
203 | padding: 10px;
204 | text-align: center;
205 | }
206 | #app .ivu-card .dataac-info .ivu-card-body,
207 | #app .ivu-card .dataac-err .ivu-card-body,
208 | #app .ivu-card .dataac-warn .ivu-card-body
209 | {
210 | font-size: 18px;
211 | font-weight: bold;
212 | }
213 | .ivu-card pre{
214 | overflow: scroll;
215 | }
216 | .dataac-subText{
217 | color: #666;
218 | }
219 | .dataac-cardContent .ivu-card-head{
220 | padding: 16px 16px;
221 | }
222 | .ivu-menu-item span, .ivu-menu-submenu-title span{
223 | color: #dbfdfb;
224 | }
225 | .dataac-logo{
226 | width: 100%;
227 | display: inline-block;
228 | }
229 | .dataAc-emptyData{
230 | height: 100px;
231 | line-height: 100px;
232 | text-align: center;
233 | }
234 | .data-lincontent{
235 | padding: 10px;
236 | word-break: break-all;
237 | }
--------------------------------------------------------------------------------
/example/lib/styles/fonts/ionicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Li-vien/Vue-dataAc/d7c2a0a03b240212409762b62ba833057b529ff9/example/lib/styles/fonts/ionicons.ttf
--------------------------------------------------------------------------------
/example/lib/styles/fonts/ionicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Li-vien/Vue-dataAc/d7c2a0a03b240212409762b62ba833057b529ff9/example/lib/styles/fonts/ionicons.woff
--------------------------------------------------------------------------------
/example/lib/styles/fonts/ionicons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Li-vien/Vue-dataAc/d7c2a0a03b240212409762b62ba833057b529ff9/example/lib/styles/fonts/ionicons.woff2
--------------------------------------------------------------------------------
/example/lib/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/log/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 默认实例
3 | * */
4 | var app = new Vue({
5 | el: '#app',
6 | data: function(){
7 | var _this = this;
8 | return {
9 | menuData: window.__menuData__ || [],
10 | allLogData: [],
11 | uuid: '',
12 | pageSize: 0,
13 | clickSize: 0,
14 | inputSize: 0,
15 | userFoot: [],
16 | errCode: [],
17 | reqError: [],
18 | sourceError: [],
19 | promiseError: [],
20 | customError: [],
21 | vueError: [],
22 | pagePer: []
23 | }
24 | },
25 | computed: {},
26 | watch: {},
27 | methods: {
28 | formatTime: function(time) {
29 | var date = new Date(time);
30 | return ((date.getFullYear()) + "/" + (date.getMonth() + 1) + "/" + (date.getDate()) + " " + (date.getHours()) + ":" + (date.getMinutes()) + ":" + (date.getSeconds()));
31 | },
32 | refData: function(){
33 | var uuid = localStorage.getItem('vue_ac_userSha');
34 | this.uuid = uuid;
35 | this.getAllLog(uuid);
36 | },
37 | getAllLog: function(uuid, type){
38 | var _this = this;
39 | var url = 'https://data.ccedit.com/logStash/getLog?uuid=' + uuid + '&type=' + (type || '');
40 | axios.get(url)
41 | .then(function (response) {
42 | if(response.data && response.data.isOk){
43 | _this.allLogData = response.data.data || [];
44 | }else{
45 | _this.allLogData = []
46 | }
47 | _this.renderPage(_this.allLogData);
48 | })
49 | .catch(function (error) {
50 | console.log(error);
51 | });
52 | },
53 | renderPage: function(acData) {
54 | var pageSize = 0,
55 | clickSize = 0,
56 | inputSize = 0;
57 |
58 | var userFoot = [],
59 | errCode = [],
60 | reqError = [],
61 | sourceError = [],
62 | promiseError = [],
63 | customError = [],
64 | vueError = [],
65 | pagePer = [];
66 |
67 | for(var i = 0,len = acData.length; i < len; i++){
68 | for(var key in acData[i]._acData) {
69 | acData[i][key] = acData[i]._acData[key];
70 | }
71 | acData[i]['sTme'] && (acData[i]['sTme'] = this.formatTime(acData[i]['sTme']));
72 | acData[i]['inTime'] && (acData[i]['inTime'] = this.formatTime(acData[i]['inTime']));
73 | acData[i]['outTime'] && (acData[i]['outTime'] = this.formatTime(acData[i]['outTime']));
74 | switch (acData[i].type){
75 | case 'ACPAGE':
76 | pageSize++;
77 | userFoot.push(acData[i]);
78 | break;
79 | case 'ACINPUT':
80 | inputSize++;
81 | userFoot.push(acData[i]);
82 | break;
83 | case 'ACCLIK':
84 | clickSize++;
85 | userFoot.push(acData[i]);
86 | break;
87 | case 'ACRERR':
88 | reqError.push(acData[i]);
89 | break;
90 | case 'ACSCERR':
91 | sourceError.push(acData[i]);
92 | break;
93 | case 'ACPRERR':
94 | promiseError.push(acData[i]);
95 | break;
96 | case 'ACCUSTOM':
97 | customError.push(acData[i]);
98 | break;
99 | case 'ACVUERR':
100 | errCode.push(acData[i]);
101 | break;
102 | case 'ACTIME':
103 | pagePer.push(acData[i]);
104 | break;
105 | case 'ACCERR':
106 | errCode.push(acData[i]);
107 | break;
108 | case 'ACCOMP':
109 | vueError.push(acData[i]);
110 | break;
111 | }
112 | }
113 |
114 | this.pageSize = pageSize;
115 | this.inputSize = inputSize;
116 | this.clickSize = clickSize;
117 |
118 | this.userFoot = userFoot;
119 | this.errCode = errCode;
120 | this.reqError = reqError;
121 | this.sourceError = sourceError;
122 | this.promiseError = promiseError;
123 | this.customError = customError;
124 | this.vueError = vueError;
125 | this.pagePer = pagePer;
126 | }
127 | },
128 | components: {},
129 | created: function(){
130 | this.refData();
131 | },
132 | mounted: function(){
133 | //控制loading层
134 | document.getElementById('app').style.display = 'block';
135 | document.getElementById('load').style.display = 'none';
136 | },
137 | })
--------------------------------------------------------------------------------
/example/log/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Vue-dataAc 文档
9 |
10 |
11 |
20 |
21 |
22 |
23 |
82 |
83 |
加载中...
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 |
94 |
95 | {{item.text}}
96 |
97 |
98 |
99 |
100 |
103 |
104 | 暂无数据
105 |
106 |
107 | 数据统计:
108 |
109 |
110 |
111 | 用户行为统计:{{userFoot.length}} 条
112 |
113 |
114 |
115 |
116 | 请求异常统计:{{reqError.length}} 条
117 |
118 |
119 |
120 |
121 | 页面性能统计:{{pagePer.length}} 条
122 |
123 |
124 |
125 |
126 | 代码异常统计:{{errCode.length}} 条
127 |
128 |
129 |
130 |
131 | 自定义事件统计:{{customError.length}} 条
132 |
133 |
134 |
135 |
136 | 资源加载异常统计:{{sourceError.length}} 条
137 |
138 |
139 |
140 |
141 | promise异常统计:{{promiseError.length}} 条
142 |
143 |
144 |
145 |
146 | Vue组件性能统计:{{vueError.length}} 条
147 |
148 |
149 |
150 |
151 |
152 | 用户轨迹:
153 |
154 |
155 |
156 | {{item.sTme}}
157 |
158 |
159 | 用户【{{item.uuid}}】于 {{item.outTime}}
160 |
161 | 由 {{item.fromPath}}页面 前往 {{item.toPath}} 页面
162 |
163 |
164 | 访问 {{item.toPath}} 页面
165 |
166 |
167 |
168 | 用户【{{item.uuid}}】于 {{item.sTme}} 在 {{item.path}} 页面点击了
169 | id属性为:{{item.eId}} class属性为:{{item.className}} 内容为:{{item.val}} 的元素
170 |
171 |
172 | 用户【{{item.uuid}}】于 {{item.sTme}} 在 {{item.path}} 页面向
173 | id属性为:{{item.eId}} class属性为:{{item.className}} 的输入框录入了数据【{{item.val}}】
174 |
175 |
176 | 未知数据
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | Vue代码异常信息:
185 |
186 |
187 |
188 | {{item.sTme}}
189 |
190 |
191 | 用户【{{item.uuid}}】于 {{item.sTme}} 在 {{item.path}} 页面发生代码异常:
192 | 错误信息:{{item.err}}
193 | 错误摘要:{{item.msg}}
194 | 行数:{{item.line}}
195 | 列数:{{item.col}}
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | 接口异常信息:
204 |
205 |
206 |
207 | {{item.sTme}}
208 |
209 |
210 | 用户【{{item.uuid}}】于 {{item.sTme}} 在 {{item.path}} 页面发生请求异常:
211 | 异常类型:{{item.errSubType === 'time' ? '请求时间过长' : (item.errSubType === 'http'? '请求错误' : '自定义code')}}
212 | 请求接口:{{item.responseURL}}
213 | 状态码:{{item.status}}
214 | 错误描述:{{item.statusText}}
215 | 接口响应:{{item.response}}
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | 自定义事件信息:
224 |
225 |
226 |
227 | {{item.sTme}}
228 |
229 |
230 | 用户【{{item.uuid}}】于 {{item.sTme}} 在 {{item.path}} 页面上报了一个自定义事件:
231 | 自定义事件key:{{item.cusKey}}
232 | 自定义事件值:{{item.cusVal}}
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 | 组件渲染异常信息:
241 |
242 |
243 |
244 | {{item.sTme}}
245 |
246 |
247 | 用户【{{item.uuid}}】于 {{item.sTme}} 在 {{item.path}} 页面上报了一个组件渲染异常事件:
248 | 组件数据:{{item.componentsTimes}}
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 | 页面加载性能信息:
257 |
258 |
259 |
260 | {{item.sTme}}
261 |
262 |
263 | 用户【{{item.uuid}}】于 {{item.sTme}} 打开了 {{item.path}} 页面:
264 | 白屏时间:{{item.WT}}
265 | TCP连接耗时:{{item.TCP}}
266 | 执行onload事件耗时:{{item.ONL}}
267 | 所有请求耗时:{{item.ALLRT}}
268 | TTFB读取页面第一个字节的时间:{{item.TTFB}}
269 | DNS查询时间:{{item.DNS}}
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 | Promise异常数据:
278 |
279 |
280 |
281 | {{item.sTme}}
282 |
283 |
284 | 用户【{{item.uuid}}】于 {{item.sTme}} 打开 {{item.path}} 页面时发生了 Promise 异常数据
285 | 错误信息:{{item.reason}}
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 | 资源加载异常数据:
294 |
295 |
296 |
297 | {{item.sTme}}
298 |
299 |
300 | 用户【{{item.uuid}}】于 {{item.sTme}} 打开 {{item.path}} 页面时发生资源加载异常
301 | 资源地址:{{item.resourceUri}}
302 | tagName:{{item.tagName}}
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
--------------------------------------------------------------------------------
/example/performance/app.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * VueDataAc 配置
4 | * */
5 | var OPTIONS = {
6 | useImgSend: false,
7 | openClick: false,
8 | openInput: false,
9 | maxComponentLoadTime: 500
10 | }
11 | Vue.use(VueDataAc, OPTIONS)
12 |
13 |
14 | /**
15 | * 默认实例
16 | * */
17 | var app = new Vue({
18 | el: '#app',
19 | data: {
20 | menuData: window.__menuData__ || [],
21 | arrayData:[],
22 | asyncArrayData:[],
23 | },
24 | computed: {},
25 | watch: {},
26 | methods: {
27 | genBigData: function(){
28 | this.asyncArrayData = new Array(100000).keys()
29 | }
30 | },
31 | components: {},
32 | created: function(){
33 | },
34 | mounted: function(){
35 | //控制loading层
36 | document.getElementById('app').style.display = 'block';
37 | document.getElementById('load').style.display = 'none';
38 | },
39 | })
--------------------------------------------------------------------------------
/example/performance/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Vue-dataAc 文档
9 |
10 |
11 |
20 |
21 |
22 |
23 |
82 |
83 |
加载中...
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 |
94 |
95 | {{item.text}}
96 |
97 |
98 |
99 |
100 |
103 |
104 |
105 | 页面加载性能
106 |
107 | 说明:
108 |
109 | 通过performance属性获取页面时间点
110 | 保留了几个关键数据:
111 | WT: 白屏时间
112 | TCP: TCP连接耗时
113 | ONL: 执行onload事件耗时
114 | ALLRT: 所有请求耗时
115 | TTFB: TTFB读取页面第一个字节的时间
116 | DNS: DNS查询时间
117 |
118 |
119 |
120 |
121 | Vue 首次渲染组件性能
122 |
123 |
124 |
125 |
126 | {{i}}
127 |
128 |
129 |
130 | 说明:
131 |
132 | Vue 组件性能监控,目的是发现由于真实数据超预期导致的组件渲染慢问题
133 | 分别监控了首次渲染和异步更新数据导致的渲染慢
134 | 此处示例中我们将 maxComponentLoadTime 设置为 500 毫秒,方便触发上报
135 |
136 |
137 |
138 |
139 | Vue 组件更新数据性能
140 |
141 |
142 |
143 | 点击对组件进行大数据更新
144 |
145 |
146 |
147 | {{i}}
148 |
149 |
150 |
151 |
152 | 说明:
153 |
154 | 当组件通过异步数据进行渲染时,同样监控组件渲染耗时
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/example/reportsize/app.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * VueDataAc 配置
4 | * */
5 | var OPTIONS = {
6 | useImgSend: false,
7 |
8 | openReducer: true,
9 | sizeLimit: 5,
10 |
11 | selector: '.ac_input input, .ac_input textarea', //主动输入埋点
12 | classTag: 'ac_click', //主动点击埋点
13 | maxHelpfulCount: 6, //点击查找递归次数
14 | }
15 | Vue.use(VueDataAc, OPTIONS)
16 |
17 |
18 | /**
19 | * vuerouter 测试数据
20 | * */
21 | var Home = { template: 'home page
' }
22 | var Login = { template: 'login page
' }
23 | var User = { template: 'user page
' }
24 |
25 | var routes = [
26 | { path: '/home', component: Home },
27 | { path: '/login', component: Login },
28 | { path: '/user', component: User },
29 | ]
30 | const router = new VueRouter({
31 | routes: routes
32 | })
33 | /**
34 | * 默认实例
35 | * */
36 | var app = new Vue({
37 | router,
38 | el: '#app',
39 | data: {
40 | menuData: window.__menuData__ || [],
41 | inputValue: '',
42 | inputValue2: '',
43 | inputPassword: '',
44 | desc: '',
45 | desc1: '',
46 | phone: 'apple',
47 | },
48 | computed: {},
49 | watch: {},
50 | methods: {
51 | /**
52 | * 自定义事件上报
53 | * */
54 | customEvent: function(){
55 | this.$vueDataAc.setCustomAc({
56 | cusKey: 'clickAnyButton',
57 | cusVal: 'click button ...'
58 | })
59 | }
60 | },
61 | components: {},
62 | created: function(){
63 | },
64 | mounted: function(){
65 | //控制loading层
66 | document.getElementById('app').style.display = 'block';
67 | document.getElementById('load').style.display = 'none';
68 | },
69 | })
--------------------------------------------------------------------------------
/example/reportsize/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Vue-dataAc 文档
9 |
10 |
11 |
20 |
21 |
22 |
23 |
82 |
83 |
加载中...
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 |
94 |
95 | {{item.text}}
96 |
97 |
98 |
99 |
100 |
103 |
104 |
105 | 通过配置 openReducer 节流
106 |
107 |
108 | 前面的例子中大家发现如果上报的功能都打开,上报频率会非常高
109 | 为了解决这个问题,我们加了节流配置 openReducer
110 | 当打开节流后,所有数据会存储到本地,这么做的目的是保证页面刷新数据不丢失
111 | 可以通过修改 cacheEventStorage 来改变本地存储的key
112 | 开启节流后不实时上报数据,只在下列场景上报数据:
113 | 1. 缓存的事件条目超过 sizeLimit 配置的数量
114 | 2. vue router路由变化时
115 |
116 |
117 |
118 |
119 | 路由切换采集
120 |
121 |
122 |
123 | home
124 | login
125 | user
126 |
127 |
128 |
129 |
130 |
131 | 说明:
132 |
133 | 路由切换采集生效在使用 VueRouter 的前提下,因为我们监控 $route 的变化
134 | 但是当 url 发生变换的场景下,从 A 页面跳转到相同域的 B 页面,我们同样也有一套监控生效
135 | 所以哪怕你的项目里没有使用 VueRouter 是多页面的场景,也是可以采集到页面访问(ACPAGE)信息的
136 | 你可以点击左侧不同 demo 链接查看另一种数据
137 |
138 |
139 |
140 |
141 |
142 | 输入框埋点采集
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 | 说明:
170 |
171 | 主动埋点采集,需要对采集项目增加指定 class,并通过 selector 配置传递该 class 值
172 | 很多Ui库组件库都对原生元素做了封装,所以此处的 selector 根据实际场景进行配置
173 |
174 |
175 |
176 |
177 | 点击事件采集
178 |
179 |
180 |
181 | 已埋点按钮
182 | 未埋点按钮
183 | 未埋点按钮
184 | 未埋点按钮
185 |
186 |
187 | 已埋点icon
188 |
189 |
190 | 说明:
191 |
192 | 主动埋点采集,需要对指定的元素增加指定 class, 并配置对应的 classTag
193 | 要注意,主动埋点的情况下 maxHelpfulCount 同样生效,避免递归带来的性能问题
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
--------------------------------------------------------------------------------
/example/token/app.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * VueDataAc 配置
4 | * */
5 | var OPTIONS = {
6 | useImgSend: false
7 | }
8 | Vue.use(VueDataAc, OPTIONS)
9 |
10 |
11 | /**
12 | * 默认实例
13 | * */
14 | var app = new Vue({
15 | el: '#app',
16 | data: {
17 | menuData: window.__menuData__ || [],
18 | inputValue: '',
19 | inputPassword: '',
20 | desc: '',
21 | phone: 'apple',
22 | },
23 | computed: {},
24 | watch: {},
25 | methods: {
26 | /**
27 | * 设置用户信息
28 | * */
29 | setUserToken: function(){
30 | this.$vueDataAc.setUserToken('112233')
31 | }
32 | },
33 | components: {},
34 | created: function(){
35 | },
36 | mounted: function(){
37 | //控制loading层
38 | document.getElementById('app').style.display = 'block';
39 | document.getElementById('load').style.display = 'none';
40 | },
41 | })
--------------------------------------------------------------------------------
/example/token/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Vue-dataAc 文档
9 |
10 |
11 |
20 |
21 |
22 |
23 |
82 |
83 |
加载中...
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 |
94 |
95 | {{item.text}}
96 |
97 |
98 |
99 |
100 |
103 |
104 |
105 | 关联用户信息
106 |
107 |
108 |
109 | 设置token
110 |
111 |
112 | 说明:
113 |
114 | 当我们监控到用户异常时经常会想到如何复现,所以传递用户信息是一个选择
115 | 另外也可以对用户轨迹进行串联,将用户前端后端日志打通,方便定位问题
116 | 所以我们提供 setUserToken 方法设置 token 值,该值会跟随每一个事件数据上报到后台
117 |
118 |
119 |
120 |
121 | 输入框信息采集
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | 说明:
138 |
139 | 连续输入时,会持续记录输入信息,失焦时上报数据
140 | 通过修改 openInput 打开/关闭 此功能
141 | 通过修改 selector 为 `input,textarea` 采集两种元素的输入信息
142 | 通过修改 ignoreInputType 对指定类型的输入框忽略采集,此处我们忽略了 password 类型的输入框
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-dataac",
3 | "version": "2.0.9",
4 | "description": "Vue 用户行为采集,异常数据采集,Vue组件性能监控,前端异常采集",
5 | "homepage": "https://github.com/Cc-Edit/Vue-dataAc",
6 | "author": "Cc-Edit",
7 | "license": "MIT",
8 | "main": "dist/vue-dataAc.common.js",
9 | "module": "dist/vue-dataAc.esm.js",
10 | "directories": {
11 | "example": "example"
12 | },
13 | "scripts": {
14 | "build": "npm run dist",
15 | "dist": "node build/build.js",
16 | "flow": "flow check",
17 | "serve": "http-server ./example",
18 | "test": "jasmine"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/Cc-Edit/Vue-dataAc.git"
23 | },
24 | "devDependencies": {
25 | "eslint": "^7.2.0",
26 | "http-server": "^0.12.3",
27 | "jasmine": "^3.5.0",
28 | "rollup": "^2.15.0",
29 | "rollup-plugin-buble": "^0.19.8",
30 | "rollup-plugin-commonjs": "^10.1.0",
31 | "rollup-plugin-eslint": "^7.0.0",
32 | "rollup-plugin-flow-no-whitespace": "^1.0.0",
33 | "rollup-plugin-node-resolve": "^5.2.0",
34 | "rollup-plugin-replace": "^2.2.0",
35 | "terser": "^4.7.0"
36 | },
37 | "keywords": [
38 | "vue report",
39 | "vue monitor",
40 | "用户行为采集",
41 | "异常数据采集",
42 | "组件性能监控",
43 | "前端异常",
44 | "js异常",
45 | "js监控"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/spec/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "spec",
3 | "spec_files": [
4 | "**/*[sS]pec.js"
5 | ],
6 | "helpers": [
7 | "helpers/**/*.js"
8 | ],
9 | "stopSpecOnExpectationFailure": false,
10 | "random": true
11 | }
12 |
--------------------------------------------------------------------------------
/src/config/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 全局配置
3 | * */
4 | export const BASEOPTIONS = {
5 | storeVer : '__VERSION__', //Vue 版本dataAc
6 | /**
7 | * 标识类作为数据上报的key,在后台数据分析时进行数据区分,不需要动态配置
8 | * */
9 | storeInput : "ACINPUT", //输入框行为采集标识
10 | storePage : "ACPAGE", //页面访问信息采集标识
11 | storeClick : "ACCLIK", //点击事件采集标识
12 | storeReqErr : "ACRERR", //请求异常采集标识
13 | storeTiming : "ACTIME", //页面性能采集标识
14 | storeCodeErr : "ACCERR", //代码异常采集标识
15 | storeCustom : "ACCUSTOM", //自定义事件采集标识 (2.0新增)
16 | storeSourceErr : "ACSCERR", //资源加载异常采集标识 (2.0新增)
17 | storePrmseErr : "ACPRERR", //promise抛出异常 (2.0新增)
18 | storeCompErr : "ACCOMP", //Vue组件性能监控 (2.0新增)
19 | storeVueErr : "ACVUERR", //Vue异常监控 (2.0新增)
20 |
21 | /**
22 | * 全局开关,用来修改采集内容
23 | * */
24 | userSha : 'vue_ac_userSha', //用户标识存储key,有冲突可修改
25 | useImgSend : true, //默认使用图片上报数据, false为xhr请求接口上报
26 | useStorage : true, //是否使用storage作为存储载体, 设置为 false 时使用cookie,
27 | maxDays : 365, //如果使用cookie作为存储载体,此项生效,配置cookie存储时间,默认一年
28 | openInput : true, //是否开启输入数据采集
29 | openCodeErr : true, //是否开启代码异常采集
30 | openClick : true, //是否开启点击数据采集
31 | openXhrQuery : true, //是否采集接口异常时的参数params
32 | openXhrHock : true, //是否开启xhr异常采集
33 | openPerformance : true, //是否开启页面性能采集
34 | openPage : true, //是否开启页面访问信息采集 (2.0新增)
35 | openVueErr : true, //是否开启Vue异常监控 (2.0新增)
36 | openSourceErr : true, //是否开启资源加载异常采集 (2.0新增)
37 | openPromiseErr : true, //是否开启promise异常采集 (2.0新增)
38 |
39 | /**
40 | * 因为某些场景下的数据异常,导致组件不能正常渲染或者渲染慢,有几率是因为客户硬件问题导致
41 | * 所以需要做数据采样统计后才能得出结论
42 | * */
43 | openComponent : true, //是否开启组件性能采集 (2.0新增)
44 | maxComponentLoadTime : 1000,//组件渲染时间阈值,大于此时间采集信息 (2.0新增)
45 |
46 | /**
47 | * 我们认为请求时间过长也是一种异常,有几率是因为客户网络问题导致
48 | * 所以请求超时的上报需要做采样统计后才能得出结论
49 | * 所以不算是异常或警告级别,应该算通知级别
50 | * */
51 | openXhrTimeOut : true, //是否开启请求超时上报 (2.0新增)
52 | maxRequestTime : 10000, //请求时间阈值,请求到响应大于此时间,会上报异常,openXhrTimeOut 为 false 时不生效 (2.0新增)
53 | customXhrErrCode: '', //支持自定义响应code,当接口响应中的code为指定内容时上报异常
54 |
55 | /**
56 | * 输入行为采集相关配置,通过以下配置修改要监控的输入框,
57 | * 设置 input 采集全量输入框,也可以通过修改 selector 配置实现主动埋点
58 | * 设置 selector 为 `input.isjs-ac` 为主动埋点,只会采集class为isjs-ac的输入框
59 | * ignoreInputType 存在的目的是为了从安全角度排除 type 为 password 的输入框
60 | * 有更好方案请提给我 Thanks♪(・ω・)ノ
61 | * */
62 | selector : 'input', //通过控制输入框的选择器来限定监听范围,使用document.querySelector进行选择,值参考:https://www.runoob.com/cssref/css-selectors.html
63 | ignoreInputType : ['password', 'file'], //忽略的输入框type,不建议采集密码输入框内容
64 |
65 | /**
66 | * 点击行为采集相关配置,通过 classTag 进行主动埋点和自动埋点的切换:
67 | * classTag 配置为 'isjs-ac' 只会采集 class 包含 isjs-ac 的元素
68 | * classTag 配置为 '' 会采集所有被点击的元素,当然也会导致数据量大。
69 | * */
70 | classTag : '', //主动埋点标识, 自动埋点时请配置空字符串
71 | maxHelpfulCount : 5, //为了使上报数据准确,我们会递归父元素,找到一个有样式的祖先元素,此项配置递归次数
72 |
73 | /**
74 | * 以下内容为可配置信息,影响插件逻辑功能
75 | * */
76 | imageUrl : "https://data.ccedit.com/lib/image/ac.png", //《建议》 图片上报地址(通过1*1px图片接收上报信息)
77 | postUrl : "https://data.ccedit.com/logStash/push", // 数据上报接口
78 |
79 | /**
80 | * 对上报频率的限制项 (2.0新增)
81 | * */
82 | openReducer: false, //是否开启节流,用于限制上报频率
83 | sizeLimit: 20, //操作数据超过指定条目时自动上报
84 | cacheEventStorage: 'ac_cache_data', //开启节流后数据存储key
85 | manualReport: false //手动上报,需要手动执行postAcData(),开启后 sizeLimit 配置失效
86 | }
87 |
--------------------------------------------------------------------------------
/src/install.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 暴露插件接口
3 | * */
4 | export function install(Vue, options, VueDataAc) {
5 | if (install.installed) return
6 | install.installed = true
7 |
8 | Vue.mixin({
9 | watch: {
10 | $route(to, from) {
11 | /**
12 | * 路由变化进行页面访问的采集
13 | * */
14 | if(this.$vueDataAc && this.$vueDataAc.installed && this.$vueDataAc._options.openPage){
15 | this.$vueDataAc._mixinRouterWatch(to, from, true);
16 | }
17 | }
18 | },
19 | beforeCreate: function beforeMount(){
20 | /**
21 | * 组件性能监控,可能因为某些场景下的数据异常,导致组件不能正常渲染或者渲染慢
22 | * 我们希望对每个组件进行监控生命周期耗时
23 | * */
24 | if (this.$vueDataAc && this.$vueDataAc.installed && this.$vueDataAc._options.openComponent){
25 | this.$vueDataAc._mixinComponentsPerformanceStart(this)
26 | }
27 | /**
28 | * 上报当前页面url
29 | * */
30 | if(this._uid === this.$root._uid){
31 | if(this.$vueDataAc && this.$vueDataAc.installed && this.$vueDataAc._options.openPage){
32 | this.$vueDataAc._mixinRouterWatch(null, null, false);
33 | }
34 | }
35 | },
36 | beforeDestroy() {
37 | /**
38 | * 根元素移除时手动上报,以免累计条数不满足 sizeLimit
39 | * */
40 | if (this.$vueDataAc && this.$vueDataAc.installed && this._uid === this.$root._uid) {
41 | this.$vueDataAc && this.$vueDataAc.postAcData();
42 | }
43 | },
44 | /**
45 | * 在组件渲染完成后,尝试进行事件劫持
46 | * mounted 不会保证所有的子组件也都一起被挂载
47 | * 所以使用 vm.$nextTick
48 | * */
49 | mounted() {
50 | if(this.$vueDataAc && this.$vueDataAc.installed){
51 | //input 时间监听
52 | if(this.$vueDataAc._options.openInput){
53 | this.$vueDataAc._componentLoadCount++;
54 | this.$nextTick(function () {
55 | --this.$vueDataAc._componentLoadCount === 0 && this.$vueDataAc._mixinInputEvent(this);
56 | });
57 | }
58 |
59 | //组件性能监控
60 | if(this.$vueDataAc._options.openComponent){
61 | this.$nextTick(function(){
62 | this.$vueDataAc._mixinComponentsPerformanceEnd(this);
63 | })
64 | }
65 | }
66 | },
67 | beforeUpdate() {
68 | /**
69 | * 组件性能监控,可能因为某些场景下的数据异常,导致组件不能正常渲染或者渲染慢
70 | * 我们希望对每个组件进行监控生命周期耗时
71 | * */
72 | if (this.$vueDataAc && this.$vueDataAc.installed && this.$vueDataAc._options.openComponent){
73 | this.$vueDataAc._mixinComponentsPerformanceStart(this)
74 | }
75 | },
76 | updated() {
77 | if(this.$vueDataAc && this.$vueDataAc.installed && this.$vueDataAc._options.openComponent){
78 | //组件性能监控
79 | this.$nextTick(function(){
80 | this.$vueDataAc._mixinComponentsPerformanceEnd(this);
81 | })
82 | }
83 | }
84 | })
85 |
86 | Vue.prototype.$vueDataAc = new VueDataAc(options, Vue);
87 | }
88 |
--------------------------------------------------------------------------------
/src/util/util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 判断是否为空
3 | * */
4 | export function ac_util_isNullOrEmpty(obj) {
5 | return (obj !== 0 || obj !== "0") && (obj === undefined || typeof obj === "undefined" || obj === null || obj === "null" || obj === "");
6 | }
7 |
8 | /**
9 | * 判断是否为空对象
10 | * */
11 | export function ac_util_isEmptyObject(obj) {
12 | for (const key in obj) {
13 | return false;
14 | }
15 | return true;
16 | }
17 |
18 | /**
19 | * 获取有效点击元素,埋点采集只上报埋点元素
20 | * 全量采集找有ID或class的元素
21 | * */
22 | export function ac_util_getHelpfulElement(target, options, length = 0){
23 | //向上遍历到document,此次点击失效
24 | if (Object.prototype.toString.call(target) === Object.prototype.toString.call(document)){
25 | return null;
26 | }
27 | const parentNode = target && target.parentNode;
28 | const {className = '', id} = target;
29 | const {classTag, maxHelpfulCount} = options;
30 | //主动埋点
31 | if (!ac_util_isNullOrEmpty(classTag)) {
32 | //未命中
33 | if(className.indexOf(classTag) < 0){
34 | if(ac_util_isNullOrEmpty(parentNode)){
35 | return null;
36 | }else{
37 | if(length > maxHelpfulCount){
38 | return null;
39 | }else{
40 | return ac_util_getHelpfulElement(parentNode, options, ++length)
41 | }
42 | }
43 | }else{
44 | return target
45 | }
46 | }else{
47 | //全量采集
48 | if(length > maxHelpfulCount){
49 | return null;
50 | }
51 | if (ac_util_isNullOrEmpty(className) && ac_util_isNullOrEmpty(id)) {
52 | if(ac_util_isNullOrEmpty(parentNode)){
53 | return null;
54 | }else{
55 | return ac_util_getHelpfulElement(parentNode, options, ++length)
56 | }
57 | }else{
58 | return target
59 | }
60 | }
61 | }
62 |
63 | /**
64 | * 获取元素所有属性
65 | * */
66 | export function ac_util_getAllAttr(elem) {
67 | const len = (elem.attributes ? elem.attributes.length : 0);
68 | const obj = {};
69 | if (len > 0) {
70 | for (let i = 0; i < len; i++) {
71 | const attr = elem.attributes[i];
72 | obj[attr.nodeName] = attr.nodeValue.replace(/"/igm, "'");
73 | }
74 | }
75 | return obj;
76 | }
77 |
78 | /**
79 | * 判断是否定义
80 | * @param v 变量
81 | * */
82 | export function ac_util_isDef(v) {
83 | return v !== undefined;
84 | }
85 |
86 | /**
87 | * 数据存储,可通过 useStorage 配置修改存储位置
88 | * @param name * 存储key
89 | * @param value * 存储内容
90 | * @param Day 存储时长,maxDays
91 | * @param options 配置信息
92 | * */
93 | export function ac_util_setStorage(options, name, value, Day) {
94 | if (options.useStorage) {
95 | window.localStorage.setItem(name, value);
96 | } else {
97 | if (!Day) Day = options.maxDays;
98 | const exp = new Date();
99 | exp.setTime(exp.getTime() + Day * 24 * 60 * 60000);
100 | document.cookie = `${name}=${encodeURIComponent(value)};expires=${exp.toUTCString()};path=/`;
101 | }
102 | }
103 |
104 | /**
105 | * 存储读取
106 | * @param name * 存储key
107 | * @param options 配置信息
108 | * */
109 | export function ac_util_getStorage(options, name) {
110 | if (!name) return null;
111 | if (options.useStorage) {
112 | return window.localStorage.getItem(name);
113 | } else {
114 | const arr = document.cookie.match(new RegExp("(^| )" + name + "=([^;]*)(;|$)"));
115 | if (arr && arr.length > 1) {
116 | return (decodeURIComponent(arr[2]));
117 | } else {
118 | return null;
119 | }
120 | }
121 | }
122 |
123 | /**
124 | * 存储删除
125 | * @param name * 存储key
126 | * @param options 配置信息
127 | * */
128 | export function ac_util_delStorage(options, name) {
129 | if (options.useStorage) {
130 | window.localStorage.removeItem(name);
131 | } else {
132 | ac_util_setStorage(options, name, '', -1);
133 | }
134 | }
135 |
136 | /**
137 | * 生成UUID
138 | * @param len * UUID长度,默认16
139 | * @param radix 进制,默认16
140 | * */
141 | export function ac_util_getUuid(len = 16, radix = 16) {//uuid长度以及进制
142 | const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
143 | const uuid = [];
144 | for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
145 | return uuid.join('');
146 | }
147 |
148 | /**
149 | * 获取时间戳
150 | * @return timeStamp: Number
151 | * */
152 | export function ac_util_getTime() {
153 | const date = new Date();
154 | return {
155 | timeStr: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`,
156 | timeStamp: date.getTime()
157 | }
158 | }
159 |
160 | /**
161 | * 配置项合并
162 | * */
163 | export function ac_util_mergeOption(userOpt, baseOpt) {
164 | const newOpt = {};
165 | const keys = Object.keys(baseOpt);
166 |
167 | for (let i = 0; i < keys.length; i++) {
168 | newOpt[keys[i]] = ac_util_isDef(userOpt[keys[i]]) ? userOpt[keys[i]] : baseOpt[keys[i]];
169 | }
170 |
171 | return newOpt;
172 | }
173 |
174 | /**
175 | * 配置项检查
176 | * */
177 | export function ac_util_checkOptions(options) {
178 | let flag = true;
179 | if (ac_util_isEmptyObject(options)) {
180 | ac_util_warn(`--------配置项异常:不能为空------`);
181 | return false;
182 | }
183 | const notEmpty = ['storeInput', 'storePage', 'storeClick', 'storeReqErr', 'storeTiming', 'storeCodeErr', 'storeCustom',
184 | 'storeSourceErr', 'storePrmseErr', 'storeCompErr', 'storeVueErr',
185 | 'userSha', 'useImgSend', 'useStorage', 'maxDays', 'openInput', 'openCodeErr', 'openClick', 'openXhrQuery',
186 | 'openXhrHock', 'openPerformance', 'openPage', 'openVueErr', 'openSourceErr', 'openPromiseErr', 'openComponent', 'openXhrTimeOut']
187 | notEmpty.map(key => {
188 | if (ac_util_isNullOrEmpty(options[key])) {
189 | ac_util_warn(`--------配置项【${key}】不能为空------`)
190 | flag = false;
191 | }
192 | });
193 | // 上报方式检查
194 | if (options['useImgSend']) {
195 | if (ac_util_isNullOrEmpty(options['imageUrl'])) {
196 | ac_util_warn(`--------使用图片上报数据,需要配置 【imageUrl】------`)
197 | return false;
198 | }
199 | } else {
200 | if (ac_util_isNullOrEmpty(options['postUrl'])) {
201 | ac_util_warn(`--------使用接口上报数据,需要配置 【postUrl】------`)
202 | return false;
203 | }
204 | }
205 |
206 | //输入框采集配置
207 | if (options['openInput']) {
208 | if (ac_util_isNullOrEmpty(options['selector'])) {
209 | ac_util_warn(`--------请指定输入框选择器:selector------`)
210 | return false;
211 | }
212 | }
213 | //存储配置
214 | if (options['useStorage']) {
215 | if (typeof window.localStorage === 'undefined') {
216 | ac_util_warn(`--------当前容器不支持Storage存储:useStorage------`)
217 | return false;
218 | }
219 | }
220 | return flag
221 | }
222 |
223 | /**
224 | * 警告
225 | * */
226 | export function ac_util_warn(message) {
227 | if (process.env.NODE_ENV !== 'production') {
228 | typeof console !== 'undefined' && console.warn(`[vue-dataAc] ${message}`)
229 | }
230 | }
231 |
232 | /**
233 | * 内嵌AJAX
234 | * */
235 | export function ac_util_ajax(options = {}) {
236 | let xhr, params;
237 | options.type = (options.type || "GET").toUpperCase();
238 | options.dataType = (options.dataType || "json");
239 | options.async = (options.async || true);
240 | if (options.data) {
241 | params = options.data;
242 | }
243 | if (window.XMLHttpRequest) {
244 | // 非IE6
245 | xhr = new XMLHttpRequest();
246 | if (xhr.overrideMimeType) {
247 | xhr.overrideMimeType('text/xml');
248 | }
249 | } else {
250 | //IE6及其以下版本浏览器
251 | xhr = new ActiveXObject("Microsoft.XMLHTTP");
252 | }
253 |
254 | if (options.type === "GET") {
255 | xhr.open("GET", options.url + "?" + params, options.async);
256 | xhr.send(null);
257 | } else if (options.type === "POST") {
258 | xhr.open("POST", options.url, options.async);
259 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
260 | if (params) {
261 | xhr.send(params);
262 | } else {
263 | xhr.send();
264 | }
265 | }
266 | }
267 |
268 | /**
269 | * 格式化Vue异常
270 | *
271 | * */
272 | export function ac_util_formatVueErrStack(error) {
273 | const msg = error.toString();
274 | let stack = error.stack ? error.stack
275 | .replace(/\n/gi, "") // 去掉换行
276 | .replace(/\bat\b/gi, "@")
277 | .replace(/\?[^:]+/gi, "")
278 | .replace(/^\s*|\s*$/g, "")
279 | .split("@") // 以@分割信息
280 | .slice(0, 5) //只取5条
281 | .join("&&") : '';
282 | if (stack.indexOf(msg) < 0) stack = msg + "@" + stack;
283 | return stack;
284 | }
--------------------------------------------------------------------------------