hina-cloud-js-sdk 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,648 @@
1
+ import { pageInfo, sendFirstProfile } from "./property";
2
+ import { Log, _ } from "./utils";
3
+
4
+ let { location } = window;
5
+
6
+ let ignoreTags = [
7
+ "mark",
8
+ "/mark",
9
+ "strong",
10
+ "b",
11
+ "em",
12
+ "i",
13
+ "u",
14
+ "abbr",
15
+ "ins",
16
+ "del",
17
+ "s",
18
+ "sup",
19
+ ];
20
+
21
+ let defaultTags = ["a", "div", "input", "button", "textarea"];
22
+
23
+ class AutoTrack {
24
+ constructor(autoTrackConfig, stayAutoTrackConfig, ctx) {
25
+ this.autoTrackIsUsed = false;
26
+ this.otherTag = [];
27
+ this.isTrackList = {
28
+ a: true,
29
+ button: true,
30
+ };
31
+ this.autoTrackConfig = _.extend(
32
+ {
33
+ clickAutoTrack: true,
34
+ stayAutoTrack: true,
35
+ // 返回true采集,返回false不采集
36
+ isCollectUrl: () => {
37
+ return true;
38
+ },
39
+ isCollectElement: () => {
40
+ return true;
41
+ },
42
+ isCollectInput: () => {
43
+ return false;
44
+ },
45
+ // 根据特定标签添加上报的自定义字段
46
+ addCustomProperty: () => {},
47
+ // 滚动超过4s触发,单位ms
48
+ stayDelayTime: 4000,
49
+ // 页面停留最大为5小时,单位s
50
+ maxStayPageDuration: 18000, // 18000s 默认5小时
51
+ collectTags: {
52
+ div: false,
53
+ },
54
+ trackAttr: [],
55
+ },
56
+ autoTrackConfig
57
+ );
58
+ this.stayAutoTrackConfig = _.extend(
59
+ {
60
+ // 返回true采集,返回false不采集
61
+ isCollectUrl: () => {
62
+ return true;
63
+ },
64
+ },
65
+ stayAutoTrackConfig
66
+ );
67
+ this.ctx = ctx;
68
+ this.load(autoTrackConfig);
69
+ }
70
+
71
+ load(config) {
72
+ if (_.check.isArray(config.trackAttr)) {
73
+ this.autoTrackConfig.trackAttr = config.trackAttr.filter((v) =>
74
+ _.check.isString(v)
75
+ );
76
+ this.autoTrackConfig.trackAttr.push("hn-click");
77
+ } else {
78
+ this.autoTrackConfig.trackAttr = ["hn-click"];
79
+ }
80
+
81
+ if (_.check.isObject(config.collectTags)) {
82
+ if (config.collectTags.div === true) {
83
+ this.autoTrackConfig.collectTags.div = {
84
+ ignoreTags: ignoreTags,
85
+ maxLevel: 1,
86
+ };
87
+ } else if (_.check.isObject(config.collectTags.div)) {
88
+ this.autoTrackConfig.collectTags.div.ignoreTags = ignoreTags;
89
+ if (_.check.isNumber(config.collectTags.div.maxLevel)) {
90
+ let supportDivLevel = [1, 2, 3];
91
+ if (!supportDivLevel.includes(config.collectTags.div.maxLevel)) {
92
+ this.autoTrackConfig.collectTags.div.maxLevel = 1;
93
+ }
94
+ } else {
95
+ this.autoTrackConfig.collectTags.div.maxLevel = 1;
96
+ }
97
+ } else {
98
+ this.autoTrackConfig.collectTags.div = false;
99
+ }
100
+
101
+ _.each(config.collectTags, (val, key) => {
102
+ if (key !== "div" && val) {
103
+ this.otherTag.push(key);
104
+ }
105
+ });
106
+
107
+ if (this.autoTrackConfig.clickAutoTrack === true) {
108
+ _.each(this.otherTag, (tagName) => {
109
+ if (tagName in this.isTrackList) {
110
+ this.isTrackList[tagName] = true;
111
+ }
112
+ });
113
+ }
114
+ } else {
115
+ this.autoTrackConfig.collectTags = {
116
+ div: false,
117
+ };
118
+ }
119
+ }
120
+
121
+ autoTrack(data = {}, callback) {
122
+ this.ctx.track(
123
+ "H_pageview",
124
+ _.extend(
125
+ {
126
+ H_referrer: _.getReferrer(null, true),
127
+ H_url: location.href,
128
+ H_url_path: location.pathname,
129
+ H_title: document.title,
130
+ },
131
+ data
132
+ ),
133
+ callback
134
+ );
135
+ sendFirstProfile(this.ctx.userSetOnce, true, this.ctx);
136
+ this.autoTrackIsUsed = true;
137
+ }
138
+
139
+ autoTrackSinglePage(data = {}, callback) {
140
+ let referrer;
141
+ if (this.autoTrackIsUsed) {
142
+ referrer = location.href;
143
+ } else {
144
+ referrer = _.getReferrer();
145
+ }
146
+
147
+ this.ctx.track(
148
+ "H_pageview",
149
+ _.extend(
150
+ {
151
+ H_referrer: referrer,
152
+ url: location.href,
153
+ H_url_path: location.pathname,
154
+ H_title: document.title,
155
+ },
156
+ data
157
+ ),
158
+ callback
159
+ );
160
+ sendFirstProfile(this.ctx.userSetOnce, false, this.ctx);
161
+ }
162
+
163
+ listenSinglePage() {
164
+ let isSinglePage = this.ctx.getConfig("isSinglePage");
165
+ if (isSinglePage) {
166
+ _.mitt.on("hasInit", () => {
167
+ this.onUrlChange((url) => {
168
+ let _autoTrackSinglePage = (extraData = {}) => {
169
+ if (url !== location.href) {
170
+ pageInfo.pageProp.H_referrer = url;
171
+ let data = _.extend(
172
+ {
173
+ H_url: location.href,
174
+ H_referrer: url,
175
+ },
176
+ extraData
177
+ );
178
+ this.autoTrack(data);
179
+ }
180
+ };
181
+
182
+ if (_.check.isBoolean(isSinglePage)) {
183
+ _autoTrackSinglePage();
184
+ } else if (_.check.isFunction(isSinglePage)) {
185
+ let extraData = isSinglePage();
186
+ if (_.check.isObject(extraData)) {
187
+ _autoTrackSinglePage(extraData);
188
+ } else if (extraData === true) {
189
+ _autoTrackSinglePage();
190
+ }
191
+ }
192
+ });
193
+ });
194
+ }
195
+ }
196
+
197
+ initWebClick() {
198
+ if (this.autoTrackConfig.clickAutoTrack !== true) {
199
+ return;
200
+ }
201
+
202
+ let isCurrentPageCollect = true;
203
+ if (_.check.isFunction(this.autoTrackConfig.isCollectUrl)) {
204
+ this.onUrlChange(() => {
205
+ isCurrentPageCollect = !!this.autoTrackConfig.isCollectUrl();
206
+ });
207
+ }
208
+
209
+ _.addCaptureEvent(document, "click", (e) => {
210
+ if (!isCurrentPageCollect) return;
211
+ let event = e || window.event;
212
+ if (!event) return;
213
+ let eventTarget = event.target || event.srcElement;
214
+ let target = this.getTargetElement(eventTarget, event);
215
+ if (target) {
216
+ this.emitClick(event, target);
217
+ }
218
+ });
219
+ }
220
+
221
+ emitClick(event, target, customProps = {}, callback) {
222
+ if (
223
+ _.check.isFunction(this.autoTrackConfig.isCollectElement) &&
224
+ !this.autoTrackConfig.isCollectElement(target)
225
+ ) {
226
+ return false;
227
+ }
228
+
229
+ let props = this.getClickElementInfo(target);
230
+
231
+ if (_.check.isFunction(this.autoTrackConfig.addCustomProperty)) {
232
+ let customProperty = this.autoTrackConfig.addCustomProperty(target);
233
+ if (_.check.isObject(customProperty)) {
234
+ props = _.extend(props, customProperty);
235
+ }
236
+ }
237
+
238
+ props = _.extend(props, this.getPageXYInfo(event, target), customProps);
239
+ this.ctx.track("H_WebClick", props, callback);
240
+ }
241
+
242
+ initWebStay() {
243
+ if (this.autoTrackConfig.stayAutoTrack !== true) {
244
+ return;
245
+ }
246
+
247
+ let isCurrentPageCollect = true;
248
+ if (_.check.isFunction(this.stayAutoTrackConfig.isCollectUrl)) {
249
+ this.onUrlChange(() => {
250
+ isCurrentPageCollect = !!this.stayAutoTrackConfig.isCollectUrl();
251
+ });
252
+ }
253
+
254
+ let stayDelayFn = function (cb) {
255
+ let instance = {
256
+ timer: null,
257
+ timeout: 1000,
258
+ callback: cb,
259
+ };
260
+
261
+ instance.run = function (isNoDelay = false) {
262
+ let para = {};
263
+ if (!instance.timer) {
264
+ let offsetTop =
265
+ document.documentElement?.scrollTop ||
266
+ window.pageYOffset ||
267
+ document.body.scrollTop ||
268
+ 0;
269
+ para.H_viewport_position = Math.round(offsetTop) || 0;
270
+ if (isNoDelay) {
271
+ instance.callback(para, true);
272
+ instance.timer = null;
273
+ } else {
274
+ instance.timer = setTimeout(() => {
275
+ instance.callback(para, false);
276
+ instance.timer = null;
277
+ }, instance.timeout);
278
+ }
279
+ }
280
+ };
281
+ return instance;
282
+ };
283
+
284
+ let stayDelayTime = this.autoTrackConfig.stayDelayTime;
285
+ let maxStayPageDuration = this.autoTrackConfig.maxStayPageDuration;
286
+ let ctx = this.ctx;
287
+ let stayDelay = stayDelayFn(function (para, isClosePage) {
288
+ let offsetTop =
289
+ document.documentElement?.scrollTop ||
290
+ window.pageYOffset ||
291
+ document.body.scrollTop ||
292
+ 0;
293
+ let viewOffsetTop = para.H_viewport_position;
294
+ let nowTime = new Date();
295
+ let durationTime = nowTime - this.nowTime;
296
+ if (
297
+ (durationTime > stayDelayTime && offsetTop - viewOffsetTop !== 0) ||
298
+ isClosePage
299
+ ) {
300
+ _.extend(
301
+ para,
302
+ {
303
+ event_duration: Math.min(
304
+ parseInt(durationTime) / 1000,
305
+ maxStayPageDuration
306
+ ),
307
+ },
308
+ _.info.pageProperties()
309
+ );
310
+ ctx.track("H_WebStay", para);
311
+ }
312
+ this.nowTime = nowTime;
313
+ });
314
+
315
+ stayDelay.nowTime = new Date();
316
+
317
+ _.addCaptureEvent(window, "scroll", () => {
318
+ if (isCurrentPageCollect) {
319
+ stayDelay.run();
320
+ }
321
+ });
322
+
323
+ _.addCaptureEvent(window, "unload", () => {
324
+ if (isCurrentPageCollect) {
325
+ stayDelay.run(true);
326
+ }
327
+ });
328
+ }
329
+
330
+ onUrlChange(cb) {
331
+ if (_.check.isFunction(cb)) {
332
+ cb();
333
+ _.mitt.on("urlChange", cb);
334
+ }
335
+ }
336
+
337
+ getTargetElement(eventTarget, event) {
338
+ if (!_.check.isElement(eventTarget)) {
339
+ return null;
340
+ }
341
+
342
+ if (!_.check.isString(eventTarget.tagName)) {
343
+ return null;
344
+ }
345
+
346
+ let tagName = eventTarget.tagName.toLowerCase();
347
+ if (["body", "html"].includes(tagName)) {
348
+ return null;
349
+ }
350
+
351
+ let tag = ["a", "button", "input", "textarea"].concat(this.otherTag);
352
+ if (tag.includes(tagName)) {
353
+ return eventTarget;
354
+ }
355
+
356
+ if (
357
+ tagName === "div" &&
358
+ this.autoTrackConfig.collectTags.div &&
359
+ this.isDivLevelValid(eventTarget)
360
+ ) {
361
+ let maxLevel = this.autoTrackConfig.collectTags?.div?.maxLevel || 1;
362
+ if (maxLevel > 1 || this.isCollectableDiv(eventTarget)) {
363
+ return eventTarget;
364
+ }
365
+ }
366
+ if (this.isStyleTag(tagName) && this.autoTrackConfig.collectTags.div) {
367
+ let parentTrackDiv = this.getCollectableParent(eventTarget);
368
+ if (parentTrackDiv && this.isDivLevelValid(parentTrackDiv)) {
369
+ return parentTrackDiv;
370
+ }
371
+ }
372
+
373
+ return (
374
+ this.hasElement({
375
+ event: event?.originalEvent || event,
376
+ element: eventTarget,
377
+ }) || null
378
+ );
379
+ }
380
+
381
+ isDivLevelValid(element) {
382
+ let maxLevel = this.autoTrackConfig.collectTags?.div?.maxLevel || 1;
383
+ let allDiv = element.getElementsByTagName("div");
384
+ for (let i = allDiv.length - 1; i >= 0; i--) {
385
+ if (this.getDivLevel(allDiv[i], element) > maxLevel) {
386
+ return false;
387
+ }
388
+ }
389
+ return true;
390
+ }
391
+
392
+ getDivLevel(element, rootElement) {
393
+ let path = this.getElementPath(element, true, rootElement);
394
+ let pathArr = path.split(" > ");
395
+ let ans = 0;
396
+ pathArr.forEach((tag) => {
397
+ if (tag === "div") {
398
+ ans++;
399
+ }
400
+ });
401
+ return ans;
402
+ }
403
+
404
+ getElementPath(element, ignoreId, rootElement) {
405
+ let names = [];
406
+ while (element.parentNode && _.check.isElement(element)) {
407
+ if (
408
+ element.id &&
409
+ !ignoreId &&
410
+ /^[A-Za-z][-A-Za-z0-9_:.]*$/.test(element.id)
411
+ ) {
412
+ names.unshift(element.tagName.toLowerCase() + "#" + element.id);
413
+ break;
414
+ } else {
415
+ if (rootElement && element === rootElement) {
416
+ names.unshift(element.tagName.toLowerCase());
417
+ break;
418
+ } else if (element === document.body) {
419
+ names.unshift("body");
420
+ break;
421
+ } else {
422
+ names.unshift(element.tagName.toLowerCase());
423
+ }
424
+ element = element.parentNode;
425
+ }
426
+ }
427
+ return names.join(" > ");
428
+ }
429
+
430
+ isCollectableDiv(target) {
431
+ try {
432
+ let targetChildren = target.children || [];
433
+ if (targetChildren?.length === 0) {
434
+ return true;
435
+ } else {
436
+ for (let i = 0; i < targetChildren.length; i++) {
437
+ if (targetChildren[i].nodeType !== 1) {
438
+ continue;
439
+ }
440
+ let tagName = targetChildren[i].tagName.toLowerCase();
441
+ let maxLevel = this.autoTrackConfig.collectTags?.div?.maxLevel || 1;
442
+ if ((tagName === "div" && maxLevel > 1) || this.isStyleTag(tagName)) {
443
+ if (!this.isCollectableDiv(targetChildren[i])) {
444
+ return false;
445
+ }
446
+ } else {
447
+ return false;
448
+ }
449
+ }
450
+ return true;
451
+ }
452
+ } catch (error) {
453
+ Log.log(error);
454
+ }
455
+ return false;
456
+ }
457
+
458
+ getCollectableParent(target) {
459
+ try {
460
+ let parentNode = target.parentNode;
461
+ let parentTagName = parentNode ? parentNode.tagName.toLowerCase() : "";
462
+ if (parentTagName === "body") {
463
+ return false;
464
+ }
465
+ let maxLevel = this.autoTrackConfig.collectTags?.div?.maxLevel || 1;
466
+ if (
467
+ parentTagName === "div" &&
468
+ (maxLevel > 1 || this.isCollectableDiv(parentNode))
469
+ ) {
470
+ return parentNode;
471
+ } else if (parentNode && this.isStyleTag(parentTagName)) {
472
+ return this.getCollectableParent(parentNode);
473
+ }
474
+ } catch (error) {
475
+ Log.log(error);
476
+ }
477
+ return false;
478
+ }
479
+
480
+ isStyleTag(tagName) {
481
+ if (defaultTags.includes(tagName)) {
482
+ return false;
483
+ }
484
+ if (this.autoTrackConfig.collectTags?.div?.ignoreTags?.includes(tagName)) {
485
+ return true;
486
+ }
487
+ return false;
488
+ }
489
+
490
+ hasElement(obj) {
491
+ let paths;
492
+ if (obj.event) {
493
+ let e = obj.event;
494
+ paths = e.path || e?._getPath();
495
+ } else if (obj.element) {
496
+ paths = _.getDomElementInfo(obj.element).getParents();
497
+ }
498
+
499
+ let trackAttr = this.autoTrackConfig.trackAttr;
500
+ if (_.check.isArray(paths) && paths.length > 0) {
501
+ for (let path of paths) {
502
+ let tagName = path.tagName?.toLowerCase();
503
+ if (
504
+ _.check.isElement(path) &&
505
+ path.nodeType === 1 &&
506
+ (this.isTrackList[tagName] || this.hasAttributes(path, trackAttr))
507
+ ) {
508
+ return path;
509
+ }
510
+ }
511
+ }
512
+ }
513
+
514
+ hasAttribute(element, attrName) {
515
+ if (element.hasAttribute) {
516
+ return element.hasAttribute(attrName);
517
+ } else if (element.attributes) {
518
+ return !!element.attributes[attrName]?.value;
519
+ }
520
+ }
521
+
522
+ hasAttributes(element, attrNames) {
523
+ if (_.check.isArray(attrNames)) {
524
+ let res = false;
525
+ for (let i = 0; i < attrNames.length; i++) {
526
+ let curRes = this.hasAttribute(element, attrNames[i]);
527
+ if (curRes) {
528
+ res = true;
529
+ break;
530
+ }
531
+ }
532
+ return res;
533
+ }
534
+ }
535
+
536
+ getPageXYInfo(event, target) {
537
+ if (!event) {
538
+ return {};
539
+ }
540
+
541
+ function getScroll() {
542
+ let scrollLeft =
543
+ document.body.scrollLeft || document.documentElement.scrollLeft || 0;
544
+ let scrollTop =
545
+ document.body.scrollTop || document.documentElement.scrollTop || 0;
546
+ return {
547
+ scrollLeft: scrollLeft,
548
+ scrollTop: scrollTop,
549
+ };
550
+ }
551
+
552
+ function getElementPosition() {
553
+ if (document.documentElement.getBoundingClientRect) {
554
+ let targetEle = target.getBoundingClientRect();
555
+ return {
556
+ targetEleX: targetEle.left + getScroll().scrollLeft || 0,
557
+ targetEleY: targetEle.top + getScroll().scrollTop || 0,
558
+ };
559
+ }
560
+ }
561
+
562
+ function toFixedThree(v) {
563
+ return Number(Number(v).toFixed(3));
564
+ }
565
+
566
+ function getPage(event) {
567
+ let pageX =
568
+ event.pageX ||
569
+ event.clientX + getScroll().scrollLeft ||
570
+ event.offsetX + getElementPosition().targetEleX ||
571
+ 0;
572
+ let pageY =
573
+ event.pageY ||
574
+ event.clientY + getScroll().scrollTop ||
575
+ event.offsetY + getElementPosition().targetEleY ||
576
+ 0;
577
+ return {
578
+ H_page_x: toFixedThree(pageX),
579
+ H_page_y: toFixedThree(pageY),
580
+ };
581
+ }
582
+
583
+ return getPage(event);
584
+ }
585
+
586
+ getClickElementInfo(target) {
587
+ let isCollectInput = this.autoTrackConfig.isCollectInput();
588
+ let selector = this.getDomSelector(target);
589
+ let props = _.info.getElementInfo(target, isCollectInput);
590
+
591
+ props.H_element_selector = selector || "";
592
+ props.H_element_path = this.getElementPath(target, false);
593
+ return props;
594
+ }
595
+
596
+ getDomSelector(el, arr = []) {
597
+ if (!el || !el.parentNode || !el.parentNode.children) return false;
598
+ let nodeName = el.nodeName.toLowerCase();
599
+ if (!el || nodeName === "body" || el.nodeType !== 1) {
600
+ arr.unshift("body");
601
+ return arr.join(" > ");
602
+ }
603
+ arr.unshift(this.getSelector(el));
604
+ if (
605
+ el?.getAttribute("id") &&
606
+ /^[A-Za-z][-A-Za-z0-9_:.]*$/.test(el.getAttribute("id"))
607
+ ) {
608
+ return arr.join(" > ");
609
+ }
610
+ return this.getDomSelector(el.parentNode, arr);
611
+ }
612
+
613
+ getSelector(el) {
614
+ let tagName = el.tagName.toLowerCase();
615
+ let i = -1;
616
+ if (el?.parentNode?.nodeType !== 9) {
617
+ i = this.getDomIndex(el);
618
+ }
619
+ if (
620
+ el?.getAttribute("id") &&
621
+ /^[A-Za-z][-A-Za-z0-9_:.]*$/.test(el.getAttribute("id"))
622
+ ) {
623
+ return "#" + el.getAttribute("id");
624
+ } else {
625
+ return tagName + (~i ? ":nth-of-type(" + (i + 1) + ")" : "");
626
+ }
627
+ }
628
+
629
+ getDomIndex(el) {
630
+ if (!el.parentNode) return -1;
631
+ let index = 0;
632
+ let nodeName = el.tagName.toLowerCase();
633
+ let list = el.parentNode.children;
634
+ for (let i = 0; i < list.length; i++) {
635
+ let childTagName = list[i]?.tagName?.toLowerCase();
636
+ if (childTagName === nodeName) {
637
+ if (list[i] === el) {
638
+ return index;
639
+ } else {
640
+ index++;
641
+ }
642
+ }
643
+ }
644
+ return -1;
645
+ }
646
+ }
647
+
648
+ export default AutoTrack;