@fle-sdk/event-tracking-web 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.esm.js CHANGED
@@ -1,3 +1,19 @@
1
+ function _typeof(obj) {
2
+ "@babel/helpers - typeof";
3
+
4
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
5
+ _typeof = function (obj) {
6
+ return typeof obj;
7
+ };
8
+ } else {
9
+ _typeof = function (obj) {
10
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
11
+ };
12
+ }
13
+
14
+ return _typeof(obj);
15
+ }
16
+
1
17
  /*! *****************************************************************************
2
18
  Copyright (c) Microsoft Corporation.
3
19
 
@@ -46,22 +62,6 @@ function __spreadArray(to, from) {
46
62
  return to;
47
63
  }
48
64
 
49
- function _typeof(obj) {
50
- "@babel/helpers - typeof";
51
-
52
- if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
53
- _typeof = function (obj) {
54
- return typeof obj;
55
- };
56
- } else {
57
- _typeof = function (obj) {
58
- return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
59
- };
60
- }
61
-
62
- return _typeof(obj);
63
- }
64
-
65
65
  var WebTrackingTools =
66
66
  /** @class */
67
67
  function () {
@@ -444,7 +444,7 @@ function () {
444
444
 
445
445
  this.filterSensitiveData = function (obj, sensitiveKeys) {
446
446
  if (sensitiveKeys === void 0) {
447
- sensitiveKeys = ['password', 'token', 'secret', 'key'];
447
+ sensitiveKeys = ["password", "token", "secret", "key"];
448
448
  }
449
449
 
450
450
  if (!_this.isObject(obj)) return obj;
@@ -453,11 +453,11 @@ function () {
453
453
  _this.each(obj, function (value, key) {
454
454
  // 检查是否是敏感字段
455
455
  var isSensitive = sensitiveKeys.some(function (sensitiveKey) {
456
- return typeof key === 'string' && key.toLowerCase().includes(sensitiveKey.toLowerCase());
456
+ return typeof key === "string" && key.toLowerCase().includes(sensitiveKey.toLowerCase());
457
457
  });
458
458
 
459
459
  if (isSensitive) {
460
- filteredObj[key] = '***';
460
+ filteredObj[key] = "***";
461
461
  } else if (_this.isObject(value)) {
462
462
  // 递归过滤嵌套对象
463
463
  filteredObj[key] = _this.filterSensitiveData(value, sensitiveKeys);
@@ -476,9 +476,9 @@ function () {
476
476
 
477
477
 
478
478
  this.xssFilter = function (str) {
479
- if (!str) return '';
480
- if (typeof str !== 'string') return str.toString();
481
- return str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g, '&#x2F;');
479
+ if (!str) return "";
480
+ if (typeof str !== "string") return str.toString();
481
+ return str.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;").replace(/\//g, "&#x2F;");
482
482
  };
483
483
  /**
484
484
  * 获取url中的参数
@@ -518,7 +518,7 @@ function () {
518
518
  return {};
519
519
  }
520
520
 
521
- if (typeof data === 'string') {
521
+ if (typeof data === "string") {
522
522
  try {
523
523
  return JSON.parse(data);
524
524
  } catch (e) {
@@ -526,7 +526,7 @@ function () {
526
526
  }
527
527
  }
528
528
 
529
- if (_typeof(data) === 'object') {
529
+ if (_typeof(data) === "object") {
530
530
  return data;
531
531
  }
532
532
 
@@ -817,7 +817,7 @@ function () {
817
817
 
818
818
  this.getDeviceId = function () {
819
819
  // 获取已存储的设备ID
820
- var storedDeviceId = _this.getCookie('device_id') || _this.getLocalStorage('device_id');
820
+ var storedDeviceId = _this.getCookie("device_id") || _this.getLocalStorage("device_id");
821
821
 
822
822
  if (storedDeviceId) {
823
823
  return storedDeviceId;
@@ -830,10 +830,10 @@ function () {
830
830
  var deviceId = _this.hashFingerprint(fingerprint); // 存储设备ID
831
831
 
832
832
 
833
- _this.setCookie('device_id', deviceId, 365 * 2); // 存储2年
833
+ _this.setCookie("device_id", deviceId, 365 * 2); // 存储2年
834
834
 
835
835
 
836
- _this.setLocalStorage('device_id', deviceId);
836
+ _this.setLocalStorage("device_id", deviceId);
837
837
 
838
838
  return deviceId;
839
839
  };
@@ -890,44 +890,50 @@ function () {
890
890
 
891
891
  this.getWebGLFingerprint = function () {
892
892
  try {
893
- var canvas = document.createElement('canvas');
894
- var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
895
- if (!gl) return 'not-supported';
896
- var debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
897
- var vendor = debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : 'unknown';
898
- var renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : 'unknown';
893
+ var canvas = document.createElement("canvas");
894
+ var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
895
+ if (!gl) return "not-supported";
896
+ var debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
897
+ var vendor = debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : "unknown";
898
+ var renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : "unknown";
899
899
  return vendor + "|" + renderer;
900
900
  } catch (e) {
901
- return 'error';
901
+ return "error";
902
902
  }
903
903
  };
904
904
  /**
905
905
  * 获取Canvas指纹
906
+ * 注意:使用固定的尺寸和绘制参数,确保在不同时间生成一致的指纹
906
907
  * @returns Canvas指纹字符串
907
908
  */
908
909
 
909
910
 
910
911
  this.getCanvasFingerprint = function () {
911
912
  try {
912
- var canvas = document.createElement('canvas');
913
- var ctx = canvas.getContext('2d');
914
- if (!ctx) return 'not-supported'; // 绘制特定图形
915
-
916
- ctx.textBaseline = 'top';
917
- ctx.font = '14px Arial';
918
- ctx.fillStyle = '#f60';
913
+ // 使用固定的 canvas 尺寸,确保一致性
914
+ var canvas = document.createElement("canvas");
915
+ canvas.width = 200;
916
+ canvas.height = 50;
917
+ var ctx = canvas.getContext("2d");
918
+ if (!ctx) return "not-supported"; // 使用固定的绘制参数,确保每次生成一致
919
+
920
+ ctx.textBaseline = "top";
921
+ ctx.font = "14px Arial";
922
+ ctx.fillStyle = "#f60";
919
923
  ctx.fillRect(125, 1, 62, 20);
920
- ctx.fillStyle = '#069';
921
- ctx.fillText('Canvas fingerprint', 2, 15);
922
- ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
923
- ctx.fillText('Canvas fingerprint', 4, 17);
924
- return canvas.toDataURL().slice(-50); // 取后50个字符
924
+ ctx.fillStyle = "#069";
925
+ ctx.fillText("Canvas fingerprint", 2, 15);
926
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
927
+ ctx.fillText("Canvas fingerprint", 4, 17); // 取后50个字符作为指纹
928
+
929
+ return canvas.toDataURL().slice(-50);
925
930
  } catch (e) {
926
- return 'error';
931
+ return "error";
927
932
  }
928
933
  };
929
934
  /**
930
935
  * 获取音频上下文指纹
936
+ * 注意:只使用稳定的 sampleRate,不使用 currentTime(会随时间变化)
931
937
  * @returns 音频指纹字符串
932
938
  */
933
939
 
@@ -935,26 +941,14 @@ function () {
935
941
  this.getAudioFingerprint = function () {
936
942
  try {
937
943
  var AudioContextClass = window.AudioContext || window.webkitAudioContext;
938
- if (!AudioContextClass) return 'not-supported';
939
- var context = new AudioContextClass();
940
- var oscillator = context.createOscillator();
941
- var analyser = context.createAnalyser();
942
- var gain = context.createGain();
943
- var scriptProcessor = context.createScriptProcessor(4096, 1, 1);
944
- oscillator.type = 'triangle';
945
- oscillator.frequency.value = 10000;
946
- gain.gain.value = 0;
947
- oscillator.connect(analyser);
948
- analyser.connect(scriptProcessor);
949
- scriptProcessor.connect(gain);
950
- gain.connect(context.destination);
951
- oscillator.start(0);
952
- var fingerprint = context.sampleRate + '|' + context.currentTime;
953
- oscillator.stop();
944
+ if (!AudioContextClass) return "not-supported";
945
+ var context = new AudioContextClass(); // 只使用稳定的 sampleRate,不使用 currentTime(会随时间变化导致指纹不一致)
946
+
947
+ var fingerprint = String(context.sampleRate || 0);
954
948
  context.close();
955
949
  return fingerprint;
956
950
  } catch (e) {
957
- return 'error';
951
+ return "error";
958
952
  }
959
953
  };
960
954
  /**
@@ -965,15 +959,15 @@ function () {
965
959
 
966
960
  this.getFontFingerprint = function () {
967
961
  try {
968
- var baseFonts_1 = ['monospace', 'sans-serif', 'serif'];
969
- var testString_1 = 'mmmmmmmmmmlli';
970
- var testSize_1 = '72px';
971
- var canvas = document.createElement('canvas');
972
- var ctx_1 = canvas.getContext('2d');
973
- if (!ctx_1) return 'not-supported';
962
+ var baseFonts_1 = ["monospace", "sans-serif", "serif"];
963
+ var testString_1 = "mmmmmmmmmmlli";
964
+ var testSize_1 = "72px";
965
+ var canvas = document.createElement("canvas");
966
+ var ctx_1 = canvas.getContext("2d");
967
+ if (!ctx_1) return "not-supported";
974
968
  var detectedFonts_1 = []; // 测试字体列表
975
969
 
976
- var fonts = ['Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia', 'Helvetica', 'Impact', 'Times New Roman', 'Trebuchet MS', 'Verdana']; // 获取基准宽度
970
+ var fonts = ["Arial", "Arial Black", "Comic Sans MS", "Courier New", "Georgia", "Helvetica", "Impact", "Times New Roman", "Trebuchet MS", "Verdana"]; // 获取基准宽度
977
971
 
978
972
  var baseWidths_1 = {};
979
973
  baseFonts_1.forEach(function (font) {
@@ -996,9 +990,9 @@ function () {
996
990
  detectedFonts_1.push(font);
997
991
  }
998
992
  });
999
- return detectedFonts_1.join(',');
993
+ return detectedFonts_1.join(",");
1000
994
  } catch (e) {
1001
- return 'error';
995
+ return "error";
1002
996
  }
1003
997
  };
1004
998
  /**
@@ -1021,9 +1015,9 @@ function () {
1021
1015
  }
1022
1016
  }
1023
1017
 
1024
- return plugins.join(';');
1018
+ return plugins.join(";");
1025
1019
  } catch (e) {
1026
- return 'error';
1020
+ return "error";
1027
1021
  }
1028
1022
  };
1029
1023
  /**
@@ -1034,7 +1028,7 @@ function () {
1034
1028
 
1035
1029
  this.hasLocalStorage = function () {
1036
1030
  try {
1037
- var test = '__test__';
1031
+ var test = "__test__";
1038
1032
  localStorage.setItem(test, test);
1039
1033
  localStorage.removeItem(test);
1040
1034
  return true;
@@ -1050,7 +1044,7 @@ function () {
1050
1044
 
1051
1045
  this.hasSessionStorage = function () {
1052
1046
  try {
1053
- var test = '__test__';
1047
+ var test = "__test__";
1054
1048
  sessionStorage.setItem(test, test);
1055
1049
  sessionStorage.removeItem(test);
1056
1050
  return true;
@@ -1065,7 +1059,7 @@ function () {
1065
1059
 
1066
1060
 
1067
1061
  this.hasIndexedDB = function () {
1068
- return 'indexedDB' in window && indexedDB !== null;
1062
+ return "indexedDB" in window && indexedDB !== null;
1069
1063
  };
1070
1064
  /**
1071
1065
  * 获取网络连接指纹
@@ -1076,10 +1070,11 @@ function () {
1076
1070
  this.getConnectionFingerprint = function () {
1077
1071
  try {
1078
1072
  var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
1079
- if (!connection) return 'not-supported';
1080
- return connection.effectiveType + "|" + connection.downlink + "|" + connection.rtt;
1073
+ if (!connection) return "not-supported"; // 只使用稳定的 effectiveType,不使用 downlink 和 rtt(会随网络状态变化导致指纹不一致)
1074
+
1075
+ return connection.effectiveType || "unknown";
1081
1076
  } catch (e) {
1082
- return 'error';
1077
+ return "error";
1083
1078
  }
1084
1079
  };
1085
1080
  /**
@@ -1102,12 +1097,14 @@ function () {
1102
1097
  hash1 = (hash1 << 5) + hash1 + char; // hash1 * 33 + char
1103
1098
 
1104
1099
  hash2 = (hash2 << 5) + hash2 + char; // hash2 * 33 + char
1105
- } // 组合两个哈希值并转换为64位整数
1100
+ } // 将两个32位哈希值转换为十六进制字符串并拼接,保持完整长度
1101
+
1106
1102
 
1103
+ var hash1Hex = (hash1 >>> 0).toString(16).padStart(8, '0'); // 32位 = 8个十六进制字符
1107
1104
 
1108
- var combinedHash = (hash1 >>> 0) * 4096 + (hash2 >>> 0); // 转换为36进制并添加前缀,增加长度和唯一性
1105
+ var hash2Hex = (hash2 >>> 0).toString(16).padStart(8, '0'); // 32位 = 8个十六进制字符
1109
1106
 
1110
- var deviceId = 'fp_' + combinedHash.toString(36);
1107
+ var deviceId = "fp_" + hash1Hex + hash2Hex;
1111
1108
  return deviceId;
1112
1109
  };
1113
1110
  }
@@ -1239,26 +1236,28 @@ function (_super) {
1239
1236
  __extends(WebTracking, _super);
1240
1237
 
1241
1238
  function WebTracking() {
1242
- var _this = _super.call(this) || this; // 批量发送队列
1239
+ var _this = _super.call(this) || this; // 批量发送定时器
1243
1240
 
1244
1241
 
1245
- _this.batchQueue = []; // 批量发送定时器
1246
-
1247
1242
  _this.batchTimer = null; // LocalStorage 存储 key
1248
1243
 
1249
1244
  _this.BATCH_QUEUE_STORAGE_KEY = "web_tracking_batch_queue"; // 是否使用自定义 pageKey(如果为 true,路由变化时不会自动更新 pageKey)
1250
1245
 
1251
- _this.useCustomPageKey = false; // 待发送的单个请求队列(用于页面跳转时发送)
1246
+ _this.useCustomPageKey = false; // 页面卸载监听器是否已设置
1252
1247
 
1253
- _this.pendingRequests = []; // 页面卸载监听器是否已设置
1248
+ _this.isUnloadListenerSetup = false; // 定时上报页面停留时长的定时器
1254
1249
 
1255
- _this.isUnloadListenerSetup = false; // LocalStorage 存储 key(待发送请求)
1250
+ _this.pageDurationTimer = null; // LocalStorage 存储 key(待发送请求)
1256
1251
 
1257
1252
  _this.PENDING_REQUESTS_STORAGE_KEY = "web_tracking_pending_requests"; // 待发送请求队列最大大小(默认值,可通过配置覆盖)
1258
1253
 
1259
1254
  _this.DEFAULT_PENDING_REQUESTS_MAX_SIZE = 50; // LocalStorage 最大大小限制(4MB)
1260
1255
 
1261
- _this.MAX_STORAGE_SIZE = 4 * 1024 * 1024; // 用户信息
1256
+ _this.MAX_STORAGE_SIZE = 4 * 1024 * 1024; // IntersectionObserver 实例
1257
+
1258
+ _this.exposureObserver = null; // 曝光元素映射
1259
+
1260
+ _this.exposureElementsMap = new Map(); // 用户信息
1262
1261
 
1263
1262
  _this.userInfo = null; // 当前路由
1264
1263
 
@@ -1272,7 +1271,8 @@ function (_super) {
1272
1271
  PageView: "Web 浏览页面",
1273
1272
  WebClick: "Web 元素点击",
1274
1273
  PageRetained: "Web 页面浏览时长",
1275
- CustomTrack: "Web 自定义代码上报"
1274
+ CustomTrack: "Web 自定义代码上报",
1275
+ WebExposure: "Web 元素曝光"
1276
1276
  };
1277
1277
  /**
1278
1278
  * @description 初始化函数
@@ -1312,10 +1312,19 @@ function (_super) {
1312
1312
  _this.restorePendingRequestsFromStorage(); // 无论是否启用批量发送,都需要监听页面卸载事件,确保数据发送
1313
1313
 
1314
1314
 
1315
- _this.setupBeforeUnloadListener();
1315
+ _this.setupBeforeUnloadListener(); // 如果启用了定时上报,启动定时器
1316
+
1317
+
1318
+ if (_this.initConfig.autoTrackPageDurationInterval) {
1319
+ _this.startPageDurationTimer();
1320
+ } // 如果启用了曝光监听,初始化曝光监听
1321
+
1322
+
1323
+ if (_this.initConfig.autoTrackExposure) {
1324
+ _this.initExposureObserver();
1325
+ }
1316
1326
  };
1317
1327
  /**
1318
- * TODO: 需要判断有哪些不能被预制的参数
1319
1328
  * @description 预置参数
1320
1329
  * @param {object} PresetParams [预置参数]
1321
1330
  */
@@ -1341,8 +1350,14 @@ function (_super) {
1341
1350
  if (key === 'pageKey') return;
1342
1351
 
1343
1352
  if (_this.initConfig.hasOwnProperty(key)) {
1344
- // TODO:后面加一些校验
1345
- _this.initConfig[key] = val;
1353
+ // 参数验证
1354
+ var validationResult = _this.validateConfigParam(String(key), val);
1355
+
1356
+ if (validationResult.valid) {
1357
+ _this.initConfig[key] = val;
1358
+ } else {
1359
+ _this.printLog("\u914D\u7F6E\u53C2\u6570\u9A8C\u8BC1\u5931\u8D25: " + String(key) + " = " + val + ", \u539F\u56E0: " + validationResult.message);
1360
+ }
1346
1361
  }
1347
1362
  });
1348
1363
  }
@@ -1360,7 +1375,182 @@ function (_super) {
1360
1375
  } else {
1361
1376
  // 取消监听
1362
1377
  _this.unlistener();
1378
+ } // 处理定时上报配置
1379
+
1380
+
1381
+ if (_this.initConfig.autoTrackPageDurationInterval) {
1382
+ _this.startPageDurationTimer();
1383
+ } else {
1384
+ _this.stopPageDurationTimer();
1385
+ } // 处理曝光监听配置
1386
+
1387
+
1388
+ if (_this.initConfig.autoTrackExposure) {
1389
+ _this.initExposureObserver();
1390
+ } else {
1391
+ _this.stopExposureObserver();
1392
+ }
1393
+ };
1394
+ /**
1395
+ * @description 验证配置参数
1396
+ * @param key 参数名
1397
+ * @param value 参数值
1398
+ * @returns 验证结果
1399
+ */
1400
+
1401
+
1402
+ _this.validateConfigParam = function (key, value) {
1403
+ switch (key) {
1404
+ case 'sampleRate':
1405
+ if (typeof value !== 'number' || value < 0 || value > 1) {
1406
+ return {
1407
+ valid: false,
1408
+ message: 'sampleRate 必须是 0-1 之间的数字'
1409
+ };
1410
+ }
1411
+
1412
+ break;
1413
+
1414
+ case 'sendTimeout':
1415
+ if (typeof value !== 'number' || value <= 0) {
1416
+ return {
1417
+ valid: false,
1418
+ message: 'sendTimeout 必须是大于 0 的数字'
1419
+ };
1420
+ }
1421
+
1422
+ break;
1423
+
1424
+ case 'batchInterval':
1425
+ if (typeof value !== 'number' || value <= 0) {
1426
+ return {
1427
+ valid: false,
1428
+ message: 'batchInterval 必须是大于 0 的数字'
1429
+ };
1430
+ }
1431
+
1432
+ break;
1433
+
1434
+ case 'batchMaxSize':
1435
+ if (typeof value !== 'number' || value <= 0 || !Number.isInteger(value)) {
1436
+ return {
1437
+ valid: false,
1438
+ message: 'batchMaxSize 必须是大于 0 的整数'
1439
+ };
1440
+ }
1441
+
1442
+ break;
1443
+
1444
+ case 'pendingRequestsMaxSize':
1445
+ if (typeof value !== 'number' || value <= 0 || !Number.isInteger(value)) {
1446
+ return {
1447
+ valid: false,
1448
+ message: 'pendingRequestsMaxSize 必须是大于 0 的整数'
1449
+ };
1450
+ }
1451
+
1452
+ break;
1453
+
1454
+ case 'pageDurationInterval':
1455
+ if (typeof value !== 'number' || value <= 0) {
1456
+ return {
1457
+ valid: false,
1458
+ message: 'pageDurationInterval 必须是大于 0 的数字'
1459
+ };
1460
+ }
1461
+
1462
+ break;
1463
+
1464
+ case 'sendMethod':
1465
+ if (typeof value !== 'string' || !['auto', 'xhr', 'beacon'].includes(value)) {
1466
+ return {
1467
+ valid: false,
1468
+ message: 'sendMethod 必须是 auto、xhr 或 beacon'
1469
+ };
1470
+ }
1471
+
1472
+ break;
1473
+
1474
+ case 'exposureThreshold':
1475
+ if (typeof value !== 'number' || value < 0 || value > 1) {
1476
+ return {
1477
+ valid: false,
1478
+ message: 'exposureThreshold 必须是 0-1 之间的数字'
1479
+ };
1480
+ }
1481
+
1482
+ break;
1483
+
1484
+ case 'exposureTime':
1485
+ if (typeof value !== 'number' || value <= 0) {
1486
+ return {
1487
+ valid: false,
1488
+ message: 'exposureTime 必须是大于 0 的数字'
1489
+ };
1490
+ }
1491
+
1492
+ break;
1493
+
1494
+ case 'exposureNum':
1495
+ if (value !== undefined && (typeof value !== 'number' || value <= 0 || !Number.isInteger(value))) {
1496
+ return {
1497
+ valid: false,
1498
+ message: 'exposureNum 必须是大于 0 的整数或不限制'
1499
+ };
1500
+ }
1501
+
1502
+ break;
1503
+
1504
+ case 'showLog':
1505
+ case 'autoTrack':
1506
+ case 'isTrackSinglePage':
1507
+ case 'batchSend':
1508
+ case 'trackPartKeyClick':
1509
+ case 'autoTrackPageDurationInterval':
1510
+ if (typeof value !== 'boolean') {
1511
+ return {
1512
+ valid: false,
1513
+ message: key + " \u5FC5\u987B\u662F\u5E03\u5C14\u503C"
1514
+ };
1515
+ }
1516
+
1517
+ break;
1518
+
1519
+ case 'business':
1520
+ case 'header':
1521
+ if (value !== null && _typeof(value) !== 'object') {
1522
+ return {
1523
+ valid: false,
1524
+ message: key + " \u5FC5\u987B\u662F\u5BF9\u8C61\u6216 null"
1525
+ };
1526
+ }
1527
+
1528
+ break;
1529
+
1530
+ case 'contentType':
1531
+ if (value !== 'application/json' && value !== 'application/x-www-form-urlencoded') {
1532
+ return {
1533
+ valid: false,
1534
+ message: 'contentType 必须是 application/json 或 application/x-www-form-urlencoded'
1535
+ };
1536
+ }
1537
+
1538
+ break;
1539
+
1540
+ case 'platform':
1541
+ if (typeof value !== 'string') {
1542
+ return {
1543
+ valid: false,
1544
+ message: 'platform 必须是字符串'
1545
+ };
1546
+ }
1547
+
1548
+ break;
1363
1549
  }
1550
+
1551
+ return {
1552
+ valid: true
1553
+ };
1364
1554
  };
1365
1555
  /**
1366
1556
  * 用户登录
@@ -1454,7 +1644,10 @@ function (_super) {
1454
1644
 
1455
1645
 
1456
1646
  _this.listener = function () {
1457
- // 如果启用了全埋点,监听页面浏览事件
1647
+ // 先移除旧的监听器,避免重复绑定
1648
+ _this.unlistener(); // 如果启用了全埋点,监听页面浏览事件
1649
+
1650
+
1458
1651
  if (!!_this.initConfig.autoTrack) {
1459
1652
  if (!!_this.initConfig.isTrackSinglePage) {
1460
1653
  _this.rewriteHistory();
@@ -1494,7 +1687,10 @@ function (_super) {
1494
1687
  _this.removeEventListener(window, "click", _this.onClickCallback); // 清理批量发送定时器
1495
1688
 
1496
1689
 
1497
- _this.clearBatchTimer();
1690
+ _this.clearBatchTimer(); // 清理定时上报定时器
1691
+
1692
+
1693
+ _this.stopPageDurationTimer();
1498
1694
  };
1499
1695
  /**
1500
1696
  * @description 清理批量发送定时器
@@ -1513,14 +1709,104 @@ function (_super) {
1513
1709
 
1514
1710
 
1515
1711
  _this.clearBatchQueue = function () {
1516
- _this.batchQueue = [];
1517
-
1518
1712
  _this.setLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY, "[]");
1519
1713
 
1520
1714
  if (_this.initConfig.showLog) {
1521
1715
  _this.printLog("批量队列已清空");
1522
1716
  }
1523
1717
  };
1718
+ /**
1719
+ * @description 从 LocalStorage 获取批量队列
1720
+ * @returns 批量队列数组
1721
+ */
1722
+
1723
+
1724
+ _this.getBatchQueueFromStorage = function () {
1725
+ try {
1726
+ var storedQueue = _this.getLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY);
1727
+
1728
+ if (storedQueue) {
1729
+ var parsedQueue = JSON.parse(storedQueue);
1730
+
1731
+ if (Array.isArray(parsedQueue)) {
1732
+ return parsedQueue;
1733
+ }
1734
+ }
1735
+ } catch (e) {
1736
+ _this.printLog("\u8BFB\u53D6\u6279\u91CF\u961F\u5217\u5931\u8D25: " + e); // 如果解析失败,清除损坏的数据
1737
+
1738
+
1739
+ _this.setLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY, "[]");
1740
+ }
1741
+
1742
+ return [];
1743
+ };
1744
+ /**
1745
+ * @description 保存批量队列到 LocalStorage
1746
+ * @param queue 批量队列数组
1747
+ */
1748
+
1749
+
1750
+ _this.saveBatchQueueToStorage = function (queue) {
1751
+ try {
1752
+ var queueString = JSON.stringify(queue); // 检查存储大小,避免超出 LocalStorage 限制
1753
+
1754
+ if (queueString.length > _this.MAX_STORAGE_SIZE) {
1755
+ var maxItems = Math.floor(queue.length * 0.8); // 保留 80%
1756
+
1757
+ var trimmedQueue = queue.slice(-maxItems);
1758
+
1759
+ _this.printLog("\u961F\u5217\u8FC7\u5927\uFF0C\u5DF2\u622A\u65AD\u4FDD\u7559\u6700\u65B0 " + maxItems + " \u6761\u6570\u636E\uFF08\u9650\u5236: " + _this.MAX_STORAGE_SIZE / 1024 / 1024 + "MB\uFF09");
1760
+
1761
+ _this.setLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY, JSON.stringify(trimmedQueue));
1762
+ } else {
1763
+ _this.setLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY, queueString);
1764
+ }
1765
+ } catch (e) {
1766
+ // LocalStorage 可能已满或不可用
1767
+ _this.printLog("\u4FDD\u5B58\u6279\u91CF\u961F\u5217\u5230 LocalStorage \u5931\u8D25: " + e);
1768
+ }
1769
+ };
1770
+ /**
1771
+ * @description 从 LocalStorage 获取待发送请求队列
1772
+ * @returns 待发送请求队列数组
1773
+ */
1774
+
1775
+
1776
+ _this.getPendingRequestsFromStorage = function () {
1777
+ try {
1778
+ var storedRequests = _this.getLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY);
1779
+
1780
+ if (storedRequests) {
1781
+ var parsedRequests = JSON.parse(storedRequests);
1782
+
1783
+ if (Array.isArray(parsedRequests)) {
1784
+ return parsedRequests;
1785
+ }
1786
+ }
1787
+ } catch (e) {
1788
+ _this.printLog("\u8BFB\u53D6\u5F85\u53D1\u9001\u8BF7\u6C42\u5931\u8D25: " + e); // 如果解析失败,清除损坏的数据
1789
+
1790
+
1791
+ _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, "[]");
1792
+ }
1793
+
1794
+ return [];
1795
+ };
1796
+ /**
1797
+ * @description 保存待发送请求队列到 LocalStorage
1798
+ * @param requests 待发送请求队列数组
1799
+ */
1800
+
1801
+
1802
+ _this.savePendingRequestsToStorage = function (requests) {
1803
+ try {
1804
+ _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, JSON.stringify(requests));
1805
+ } catch (e) {
1806
+ // LocalStorage 可能已满或不可用
1807
+ _this.printLog("\u4FDD\u5B58\u5F85\u53D1\u9001\u8BF7\u6C42\u5230 LocalStorage \u5931\u8D25: " + e);
1808
+ }
1809
+ };
1524
1810
  /**
1525
1811
  * @description 设置自定义页面唯一标识
1526
1812
  * @param pageKey 页面唯一标识,如果传入 null 或空字符串,则恢复自动生成
@@ -1589,7 +1875,7 @@ function (_super) {
1589
1875
  }
1590
1876
  });
1591
1877
 
1592
- return _this.sendData(params);
1878
+ _this.sendData(params);
1593
1879
  };
1594
1880
  /**
1595
1881
  * @description 路由触发事件
@@ -1600,7 +1886,11 @@ function (_super) {
1600
1886
  var _a, _b; // 在路由变化前,先发送待发送的数据(避免被取消)
1601
1887
 
1602
1888
 
1603
- if (_this.pendingRequests.length > 0 || _this.initConfig.batchSend && _this.batchQueue.length > 0) {
1889
+ var pendingRequests = _this.getPendingRequestsFromStorage();
1890
+
1891
+ var batchQueue = _this.initConfig.batchSend ? _this.getBatchQueueFromStorage() : [];
1892
+
1893
+ if (pendingRequests.length > 0 || batchQueue.length > 0) {
1604
1894
  _this.flushPendingData();
1605
1895
  }
1606
1896
 
@@ -1619,6 +1909,13 @@ function (_super) {
1619
1909
 
1620
1910
  if (!_this.useCustomPageKey) {
1621
1911
  _this.pageKey = window.location.pathname.replace(/\//g, "_").substring(1);
1912
+ } // 路由变化时,如果启用了定时上报,需要重启定时器
1913
+
1914
+
1915
+ if (_this.initConfig.autoTrackPageDurationInterval) {
1916
+ _this.stopPageDurationTimer();
1917
+
1918
+ _this.startPageDurationTimer();
1622
1919
  }
1623
1920
 
1624
1921
  _this.sendRetained(e.type);
@@ -1708,16 +2005,42 @@ function (_super) {
1708
2005
 
1709
2006
 
1710
2007
  _this.flushBatchQueue = function () {
1711
- if (_this.batchQueue.length === 0) return;
2008
+ var batchQueue = _this.getBatchQueueFromStorage();
2009
+
2010
+ if (batchQueue.length === 0) return;
2011
+
2012
+ var currentTime = _this.getTimeStamp(); // 过滤出可以发送的数据(未到重试时间的不发送)
2013
+
2014
+
2015
+ var readyToSend = batchQueue.filter(function (item) {
2016
+ if (!item._nextRetryTime) {
2017
+ return true;
2018
+ }
2019
+
2020
+ return item._nextRetryTime <= currentTime;
2021
+ });
2022
+
2023
+ if (readyToSend.length === 0) {
2024
+ if (_this.initConfig.showLog) {
2025
+ _this.printLog("\u6279\u91CF\u961F\u5217\u4E2D\u6709 " + batchQueue.length + " \u6761\u6570\u636E\u7B49\u5F85\u91CD\u8BD5");
2026
+ }
1712
2027
 
1713
- var batchData = __spreadArray([], _this.batchQueue);
2028
+ return;
2029
+ } // 从队列中移除已准备发送的数据
2030
+
2031
+
2032
+ var remainingQueue = batchQueue.filter(function (item) {
2033
+ if (!item._nextRetryTime) {
2034
+ return false;
2035
+ }
1714
2036
 
1715
- _this.batchQueue = []; // 从 LocalStorage 中移除已发送的数据
2037
+ return item._nextRetryTime > currentTime;
2038
+ }); // 保存剩余的队列
1716
2039
 
1717
- _this.saveBatchQueueToStorage(); // 发送批量数据
2040
+ _this.saveBatchQueueToStorage(remainingQueue); // 发送批量数据
1718
2041
 
1719
2042
 
1720
- _this.sendBatchData(batchData);
2043
+ _this.sendBatchData(readyToSend);
1721
2044
  };
1722
2045
  /**
1723
2046
  * 发送批量数据
@@ -1729,7 +2052,9 @@ function (_super) {
1729
2052
  var _a = _this.initConfig,
1730
2053
  serverUrl = _a.serverUrl,
1731
2054
  contentType = _a.contentType,
1732
- showLog = _a.showLog;
2055
+ showLog = _a.showLog,
2056
+ sendMethod = _a.sendMethod,
2057
+ initHeader = _a.header;
1733
2058
 
1734
2059
  if (showLog) {
1735
2060
  _this.printLog("\u6279\u91CF\u53D1\u9001 " + data.length + " \u6761\u6570\u636E");
@@ -1737,120 +2062,200 @@ function (_super) {
1737
2062
  data.forEach(function (item) {
1738
2063
  return _this.printLog(item);
1739
2064
  });
1740
- } // 这里可以根据实际需求决定是逐条发送还是打包发送
1741
- // 这里选择打包发送
1742
-
1743
-
1744
- _this.ajax({
1745
- url: serverUrl,
1746
- type: "POST",
1747
- data: JSON.stringify({
1748
- events: data
1749
- }),
1750
- contentType: contentType,
1751
- credentials: false,
1752
- timeout: _this.initConfig.sendTimeout,
1753
- cors: true,
1754
- success: function success() {
1755
- // 批量发送成功,数据已从队列中移除,无需额外操作
1756
- if (_this.initConfig.showLog) {
1757
- _this.printLog("\u6279\u91CF\u53D1\u9001\u6210\u529F: " + data.length + " \u6761\u6570\u636E");
1758
- }
1759
- },
1760
- error: function error(err) {
1761
- var _a; // 批量发送失败,重新加入队列以便重试
2065
+ } // 判断是否使用 sendBeacon
1762
2066
 
1763
2067
 
1764
- _this.printLog("\u6279\u91CF\u53D1\u9001\u5931\u8D25: " + err + "\uFF0C\u6570\u636E\u5DF2\u91CD\u65B0\u52A0\u5165\u961F\u5217"); // 将失败的数据重新加入队列,避免数据丢失
2068
+ var shouldUseBeacon = _this.shouldUseBeacon(sendMethod, undefined, initHeader); // 如果使用 sendBeacon
1765
2069
 
1766
2070
 
1767
- (_a = _this.batchQueue).unshift.apply(_a, data); // 限制重试队列大小,避免内存溢出
2071
+ if (shouldUseBeacon) {
2072
+ try {
2073
+ var blob = new Blob([JSON.stringify(data)], {
2074
+ type: contentType || "application/json"
2075
+ });
2076
+ var sent = navigator.sendBeacon(serverUrl, blob);
1768
2077
 
2078
+ if (sent) {
2079
+ // 发送成功,确保 LocalStorage 已清空
2080
+ _this.saveBatchQueueToStorage([]);
1769
2081
 
1770
- if (_this.batchQueue.length > _this.initConfig.batchMaxSize * 2) {
1771
- _this.batchQueue = _this.batchQueue.slice(0, _this.initConfig.batchMaxSize);
1772
- } // 保存失败的数据到 LocalStorage
2082
+ if (showLog) {
2083
+ _this.printLog("\u6279\u91CF\u53D1\u9001\u6210\u529F: " + data.length + " \u6761\u6570\u636E");
2084
+ }
2085
+ } else {
2086
+ // sendBeacon 返回 false,重新加入队列以便重试
2087
+ _this.printLog("\u6279\u91CF\u53D1\u9001\u5931\u8D25: sendBeacon \u8FD4\u56DE false\uFF0C\u6570\u636E\u5DF2\u91CD\u65B0\u52A0\u5165\u961F\u5217");
1773
2088
 
2089
+ _this.retryBatchData(data);
2090
+ }
2091
+ } catch (e) {
2092
+ // sendBeacon 失败,重新加入队列以便重试
2093
+ _this.printLog("\u6279\u91CF\u53D1\u9001\u5931\u8D25: " + e + "\uFF0C\u6570\u636E\u5DF2\u91CD\u65B0\u52A0\u5165\u961F\u5217");
1774
2094
 
1775
- _this.saveBatchQueueToStorage();
2095
+ _this.retryBatchData(data);
1776
2096
  }
1777
- });
2097
+ } else {
2098
+ // 使用 XMLHttpRequest 发送
2099
+ _this.ajax({
2100
+ url: serverUrl,
2101
+ type: "POST",
2102
+ data: JSON.stringify({
2103
+ events: data
2104
+ }),
2105
+ contentType: contentType,
2106
+ credentials: false,
2107
+ timeout: _this.initConfig.sendTimeout,
2108
+ cors: true,
2109
+ success: function success() {
2110
+ // 批量发送成功,确保 LocalStorage 已清空
2111
+ // flushBatchQueue 在发送前已清空 LocalStorage,这里再次确认
2112
+ _this.saveBatchQueueToStorage([]);
2113
+
2114
+ if (_this.initConfig.showLog) {
2115
+ _this.printLog("\u6279\u91CF\u53D1\u9001\u6210\u529F: " + data.length + " \u6761\u6570\u636E");
2116
+ }
2117
+ },
2118
+ error: function error(err) {
2119
+ // 批量发送失败,重新加入队列以便重试
2120
+ _this.printLog("\u6279\u91CF\u53D1\u9001\u5931\u8D25: " + err + "\uFF0C\u6570\u636E\u5DF2\u91CD\u65B0\u52A0\u5165\u961F\u5217");
2121
+
2122
+ _this.retryBatchData(data);
2123
+ }
2124
+ });
2125
+ }
1778
2126
  };
1779
2127
  /**
1780
- * 添加到批量队列
1781
- * @param params 数据参数
2128
+ * @description 批量数据重试逻辑
2129
+ * @param data 批量数据
1782
2130
  */
1783
2131
 
1784
2132
 
1785
- _this.addToBatchQueue = function (params) {
1786
- var _a = _this.initConfig,
1787
- batchInterval = _a.batchInterval,
1788
- batchMaxSize = _a.batchMaxSize;
2133
+ _this.retryBatchData = function (data) {
2134
+ // 获取当前队列
2135
+ var currentQueue = _this.getBatchQueueFromStorage(); // 去重:基于事件类型、itemKey、requestTime 生成唯一键
1789
2136
 
1790
- _this.batchQueue.push(params); // 保存到 LocalStorage
1791
2137
 
2138
+ var getEventKey = function getEventKey(item) {
2139
+ return item.event + "_" + item.itemKey + "_" + item.requestTime;
2140
+ };
1792
2141
 
1793
- _this.saveBatchQueueToStorage(); // 如果队列达到最大数量,立即发送
2142
+ var existingKeys = new Set(currentQueue.map(getEventKey));
2143
+ var maxRetryCount = 3; // 最大重试次数
1794
2144
 
2145
+ var currentTime = _this.getTimeStamp(); // 过滤并更新重试信息
1795
2146
 
1796
- if (_this.batchQueue.length >= batchMaxSize) {
1797
- _this.flushBatchQueue();
1798
2147
 
1799
- return;
1800
- } // 设置定时发送
2148
+ var retryData = data.filter(function (item) {
2149
+ var key = getEventKey(item); // 检查是否已存在
1801
2150
 
2151
+ if (existingKeys.has(key)) {
2152
+ return false;
2153
+ } // 检查重试次数
1802
2154
 
1803
- if (!_this.batchTimer) {
1804
- _this.batchTimer = window.setTimeout(function () {
1805
- _this.flushBatchQueue();
1806
2155
 
1807
- _this.batchTimer = null;
1808
- }, batchInterval);
1809
- }
1810
- };
1811
- /**
1812
- * 从 LocalStorage 恢复批量队列
1813
- */
2156
+ var retryCount = (item._retryCount || 0) + 1;
1814
2157
 
2158
+ if (retryCount > maxRetryCount) {
2159
+ if (_this.initConfig.showLog) {
2160
+ _this.printLog("\u6570\u636E\u5DF2\u8FBE\u5230\u6700\u5927\u91CD\u8BD5\u6B21\u6570\uFF0C\u653E\u5F03\u91CD\u8BD5: " + key);
2161
+ }
1815
2162
 
1816
- _this.restoreBatchQueueFromStorage = function () {
1817
- try {
1818
- var storedQueue = _this.getLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY);
2163
+ return false;
2164
+ } // 更新重试信息
1819
2165
 
1820
- if (storedQueue) {
1821
- var parsedQueue = JSON.parse(storedQueue);
1822
2166
 
1823
- if (Array.isArray(parsedQueue) && parsedQueue.length > 0) {
1824
- _this.batchQueue = parsedQueue;
2167
+ item._retryCount = retryCount; // 指数退避:2^retryCount * 1000ms
1825
2168
 
1826
- if (_this.initConfig.showLog) {
1827
- _this.printLog("\u4ECE LocalStorage \u6062\u590D " + parsedQueue.length + " \u6761\u5F85\u53D1\u9001\u6570\u636E");
1828
- } // 恢复后立即尝试发送(如果达到条件)
2169
+ item._nextRetryTime = currentTime + Math.pow(2, retryCount) * 1000;
2170
+ existingKeys.add(key);
2171
+ return true;
2172
+ }); // 将失败的数据重新加入队列(添加到队列前面,优先重试)
1829
2173
 
2174
+ var retryQueue = __spreadArray(__spreadArray([], retryData), currentQueue); // 限制重试队列大小,避免内存溢出
1830
2175
 
1831
- var batchMaxSize = _this.initConfig.batchMaxSize;
1832
2176
 
1833
- if (_this.batchQueue.length >= batchMaxSize) {
1834
- _this.flushBatchQueue();
1835
- } else {
1836
- // 设置定时发送
1837
- var batchInterval = _this.initConfig.batchInterval;
2177
+ var maxSize = _this.initConfig.batchMaxSize * 2;
2178
+ var trimmedQueue = retryQueue.length > maxSize ? retryQueue.slice(0, maxSize) : retryQueue; // 保存失败的数据到 LocalStorage,确保数据不丢失
1838
2179
 
1839
- if (!_this.batchTimer) {
1840
- _this.batchTimer = window.setTimeout(function () {
1841
- _this.flushBatchQueue();
2180
+ _this.saveBatchQueueToStorage(trimmedQueue);
1842
2181
 
1843
- _this.batchTimer = null;
1844
- }, batchInterval);
1845
- }
1846
- }
1847
- }
1848
- }
1849
- } catch (e) {
1850
- _this.printLog("\u6062\u590D\u6279\u91CF\u961F\u5217\u5931\u8D25: " + e); // 如果解析失败,清除损坏的数据
2182
+ if (_this.initConfig.showLog) {
2183
+ _this.printLog("\u5DF2\u5C06 " + retryData.length + " \u6761\u6570\u636E\u52A0\u5165\u91CD\u8BD5\u961F\u5217");
2184
+ }
2185
+ };
2186
+ /**
2187
+ * 添加到批量队列
2188
+ * @param params 数据参数
2189
+ */
1851
2190
 
1852
2191
 
1853
- _this.setLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY, "[]");
2192
+ _this.addToBatchQueue = function (params) {
2193
+ var _a = _this.initConfig,
2194
+ batchInterval = _a.batchInterval,
2195
+ batchMaxSize = _a.batchMaxSize; // 数据采样判断(在添加到队列前判断)
2196
+
2197
+ if (!_this.shouldSample()) {
2198
+ if (_this.initConfig.showLog) {
2199
+ _this.printLog("数据已采样跳过(批量模式)");
2200
+ }
2201
+
2202
+ return;
2203
+ } // 从 LocalStorage 获取当前队列
2204
+
2205
+
2206
+ var currentQueue = _this.getBatchQueueFromStorage(); // 添加新数据
2207
+
2208
+
2209
+ currentQueue.push(params); // 保存到 LocalStorage
2210
+
2211
+ _this.saveBatchQueueToStorage(currentQueue); // 如果队列达到最大数量,立即发送
2212
+
2213
+
2214
+ if (currentQueue.length >= batchMaxSize) {
2215
+ _this.flushBatchQueue();
2216
+
2217
+ return;
2218
+ } // 设置定时发送
2219
+
2220
+
2221
+ if (!_this.batchTimer) {
2222
+ _this.batchTimer = window.setTimeout(function () {
2223
+ _this.flushBatchQueue();
2224
+
2225
+ _this.batchTimer = null;
2226
+ }, batchInterval);
2227
+ }
2228
+ };
2229
+ /**
2230
+ * 从 LocalStorage 恢复批量队列
2231
+ */
2232
+
2233
+
2234
+ _this.restoreBatchQueueFromStorage = function () {
2235
+ var batchQueue = _this.getBatchQueueFromStorage();
2236
+
2237
+ if (batchQueue.length > 0) {
2238
+ if (_this.initConfig.showLog) {
2239
+ _this.printLog("\u4ECE LocalStorage \u6062\u590D " + batchQueue.length + " \u6761\u5F85\u53D1\u9001\u6570\u636E");
2240
+ } // 恢复后立即尝试发送(如果达到条件)
2241
+
2242
+
2243
+ var batchMaxSize = _this.initConfig.batchMaxSize;
2244
+
2245
+ if (batchQueue.length >= batchMaxSize) {
2246
+ _this.flushBatchQueue();
2247
+ } else {
2248
+ // 设置定时发送
2249
+ var batchInterval = _this.initConfig.batchInterval;
2250
+
2251
+ if (!_this.batchTimer) {
2252
+ _this.batchTimer = window.setTimeout(function () {
2253
+ _this.flushBatchQueue();
2254
+
2255
+ _this.batchTimer = null;
2256
+ }, batchInterval);
2257
+ }
2258
+ }
1854
2259
  }
1855
2260
  };
1856
2261
  /**
@@ -1860,17 +2265,24 @@ function (_super) {
1860
2265
 
1861
2266
 
1862
2267
  _this.addToPendingRequests = function (params) {
1863
- _this.pendingRequests.push(params); // 限制队列大小,防止内存溢出
2268
+ // 从 LocalStorage 获取当前队列
2269
+ var currentRequests = _this.getPendingRequestsFromStorage(); // 添加新数据
2270
+
1864
2271
 
2272
+ currentRequests.push(params); // 限制队列大小,防止内存溢出
1865
2273
 
1866
2274
  var maxSize = _this.initConfig.pendingRequestsMaxSize || _this.DEFAULT_PENDING_REQUESTS_MAX_SIZE;
1867
2275
 
1868
- if (_this.pendingRequests.length > maxSize) {
1869
- _this.pendingRequests = _this.pendingRequests.slice(-maxSize);
2276
+ if (currentRequests.length > maxSize) {
2277
+ var trimmedRequests = currentRequests.slice(-maxSize);
1870
2278
 
1871
2279
  if (_this.initConfig.showLog) {
1872
2280
  _this.printLog("\u5F85\u53D1\u9001\u8BF7\u6C42\u961F\u5217\u5DF2\u6EE1\uFF0C\u5DF2\u79FB\u9664\u6700\u65E7\u7684\u6570\u636E\uFF08\u6700\u5927\u9650\u5236: " + maxSize + "\uFF09");
1873
2281
  }
2282
+
2283
+ _this.savePendingRequestsToStorage(trimmedRequests);
2284
+ } else {
2285
+ _this.savePendingRequestsToStorage(currentRequests);
1874
2286
  }
1875
2287
  };
1876
2288
  /**
@@ -1879,30 +2291,15 @@ function (_super) {
1879
2291
 
1880
2292
 
1881
2293
  _this.restorePendingRequestsFromStorage = function () {
1882
- try {
1883
- var storedRequests = _this.getLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY);
1884
-
1885
- if (storedRequests) {
1886
- var parsedRequests = JSON.parse(storedRequests);
1887
-
1888
- if (Array.isArray(parsedRequests) && parsedRequests.length > 0) {
1889
- _this.pendingRequests = parsedRequests;
1890
-
1891
- if (_this.initConfig.showLog) {
1892
- _this.printLog("\u4ECE LocalStorage \u6062\u590D " + parsedRequests.length + " \u6761\u5F85\u53D1\u9001\u8BF7\u6C42");
1893
- } // 恢复后立即尝试发送
1894
-
1895
-
1896
- if (_this.pendingRequests.length > 0) {
1897
- _this.flushPendingRequests();
1898
- }
1899
- }
1900
- }
1901
- } catch (e) {
1902
- _this.printLog("\u6062\u590D\u5F85\u53D1\u9001\u8BF7\u6C42\u5931\u8D25: " + e); // 如果解析失败,清除损坏的数据
2294
+ var pendingRequests = _this.getPendingRequestsFromStorage();
1903
2295
 
2296
+ if (pendingRequests.length > 0) {
2297
+ if (_this.initConfig.showLog) {
2298
+ _this.printLog("\u4ECE LocalStorage \u6062\u590D " + pendingRequests.length + " \u6761\u5F85\u53D1\u9001\u8BF7\u6C42");
2299
+ } // 注意:恢复后不立即发送,避免重复
2300
+ // 数据会留在 LocalStorage 中,等待下次正常发送或页面卸载时发送
2301
+ // 这样可以避免与批量队列冲突
1904
2302
 
1905
- _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, "[]");
1906
2303
  }
1907
2304
  };
1908
2305
  /**
@@ -1939,55 +2336,63 @@ function (_super) {
1939
2336
  };
1940
2337
  /**
1941
2338
  * 刷新待发送的单个请求(正常情况下的发送)
1942
- * 注意:这个方法会直接发送,不会再次添加到 pendingRequests,避免循环
2339
+ * 注意:这个方法会直接使用 ajax 发送,避免通过 sendData 导致重复
1943
2340
  */
1944
2341
 
1945
2342
 
1946
2343
  _this.flushPendingRequests = function () {
1947
- if (_this.pendingRequests.length === 0) {
1948
- return;
1949
- }
2344
+ var pendingRequests = _this.getPendingRequestsFromStorage();
1950
2345
 
1951
- var requestsToSend = __spreadArray([], _this.pendingRequests);
2346
+ if (pendingRequests.length === 0) {
2347
+ return;
2348
+ } // 清除 LocalStorage 中的待发送请求
1952
2349
 
1953
- _this.pendingRequests = []; // 清除 LocalStorage 中的待发送请求
1954
2350
 
1955
- _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, "[]"); // 尝试发送每个请求(使用 sendData,但如果失败不再添加到 pendingRequests,避免循环)
2351
+ _this.savePendingRequestsToStorage([]); // 直接使用 ajax 发送每个请求,避免通过 sendData 导致重复
1956
2352
 
1957
2353
 
1958
- requestsToSend.forEach(function (params) {
1959
- // 直接调用 sendData,但不处理失败情况(避免循环)
1960
- // 如果失败,数据会丢失,但这是可接受的,因为我们已经尝试过了
1961
- _this.sendData(params).catch(function (err) {
1962
- if (_this.initConfig.showLog) {
1963
- _this.printLog("\u5F85\u53D1\u9001\u8BF7\u6C42\u53D1\u9001\u5931\u8D25\uFF08\u4E0D\u518D\u91CD\u8BD5\uFF09: " + err);
2354
+ var _a = _this.initConfig,
2355
+ serverUrl = _a.serverUrl,
2356
+ sendTimeout = _a.sendTimeout,
2357
+ contentType = _a.contentType,
2358
+ showLog = _a.showLog,
2359
+ initHeader = _a.header;
2360
+ pendingRequests.forEach(function (params) {
2361
+ // 数据采样判断
2362
+ if (!_this.shouldSample()) {
2363
+ if (showLog) {
2364
+ _this.printLog("待发送请求已采样跳过");
1964
2365
  }
1965
- });
1966
- });
1967
- };
1968
- /**
1969
- * 保存批量队列到 LocalStorage
1970
- */
1971
-
1972
2366
 
1973
- _this.saveBatchQueueToStorage = function () {
1974
- try {
1975
- var queueString = JSON.stringify(_this.batchQueue); // 检查存储大小,避免超出 LocalStorage 限制(通常 5-10MB)
1976
- // 如果队列过大,只保留最新的数据
1977
-
1978
- if (queueString.length > _this.MAX_STORAGE_SIZE) {
1979
- var maxItems = Math.floor(_this.batchQueue.length * 0.8); // 保留 80%
2367
+ return;
2368
+ }
1980
2369
 
1981
- _this.batchQueue = _this.batchQueue.slice(-maxItems);
2370
+ if (showLog) {
2371
+ _this.printLog(params);
2372
+ } // 直接使用 ajax 发送
1982
2373
 
1983
- _this.printLog("\u961F\u5217\u8FC7\u5927\uFF0C\u5DF2\u622A\u65AD\u4FDD\u7559\u6700\u65B0 " + maxItems + " \u6761\u6570\u636E\uFF08\u9650\u5236: " + _this.MAX_STORAGE_SIZE / 1024 / 1024 + "MB\uFF09");
1984
- }
1985
2374
 
1986
- _this.setLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY, JSON.stringify(_this.batchQueue));
1987
- } catch (e) {
1988
- // LocalStorage 可能已满或不可用
1989
- _this.printLog("\u4FDD\u5B58\u6279\u91CF\u961F\u5217\u5230 LocalStorage \u5931\u8D25: " + e);
1990
- }
2375
+ _this.ajax({
2376
+ header: initHeader,
2377
+ url: serverUrl,
2378
+ type: "POST",
2379
+ data: JSON.stringify(params),
2380
+ contentType: contentType,
2381
+ credentials: false,
2382
+ timeout: sendTimeout,
2383
+ cors: true,
2384
+ success: function success() {
2385
+ if (showLog) {
2386
+ _this.printLog("待发送请求发送成功");
2387
+ }
2388
+ },
2389
+ error: function error(err) {
2390
+ if (showLog) {
2391
+ _this.printLog("\u5F85\u53D1\u9001\u8BF7\u6C42\u53D1\u9001\u5931\u8D25\uFF08\u4E0D\u518D\u91CD\u8BD5\uFF09: " + err);
2392
+ }
2393
+ }
2394
+ });
2395
+ });
1991
2396
  };
1992
2397
  /**
1993
2398
  * 设置页面卸载监听器,确保数据发送
@@ -2015,100 +2420,100 @@ function (_super) {
2015
2420
  window.addEventListener("pagehide", function () {
2016
2421
  _this.flushPendingData();
2017
2422
  });
2018
- };
2423
+ }; // 标记是否正在刷新待发送数据,避免重复发送
2424
+
2425
+
2426
+ _this.isFlushingPendingData = false;
2019
2427
  /**
2020
2428
  * 刷新待发送数据(在页面卸载/跳转时调用)
2021
2429
  */
2022
2430
 
2023
-
2024
2431
  _this.flushPendingData = function () {
2025
- // 收集所有待发送的数据
2432
+ // 如果正在刷新,避免重复执行
2433
+ if (_this.isFlushingPendingData) {
2434
+ return;
2435
+ } // 页面卸载时停止定时器
2436
+
2437
+
2438
+ _this.stopPageDurationTimer(); // 收集所有待发送的数据
2439
+
2440
+
2026
2441
  var allPendingData = []; // 如果有批量队列,添加到待发送列表
2027
2442
 
2028
- if (_this.batchQueue.length > 0) {
2029
- allPendingData.push.apply(allPendingData, _this.batchQueue);
2443
+ var batchQueue = _this.getBatchQueueFromStorage();
2444
+
2445
+ if (batchQueue.length > 0) {
2446
+ allPendingData.push.apply(allPendingData, batchQueue);
2030
2447
  } // 如果有待发送的单个请求,也添加到列表
2031
2448
 
2032
2449
 
2033
- if (_this.pendingRequests.length > 0) {
2034
- allPendingData.push.apply(allPendingData, _this.pendingRequests);
2450
+ var pendingRequests = _this.getPendingRequestsFromStorage();
2451
+
2452
+ if (pendingRequests.length > 0) {
2453
+ allPendingData.push.apply(allPendingData, pendingRequests);
2035
2454
  }
2036
2455
 
2037
2456
  if (allPendingData.length === 0) {
2038
2457
  return;
2458
+ } // 标记正在刷新
2459
+
2460
+
2461
+ _this.isFlushingPendingData = true; // 先保存到 LocalStorage,确保数据不丢失(在发送前保存)
2462
+
2463
+ try {
2464
+ if (_this.initConfig.batchSend) {
2465
+ _this.setLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY, JSON.stringify(allPendingData));
2466
+ } else {
2467
+ _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, JSON.stringify(allPendingData));
2468
+ }
2469
+ } catch (e) {
2470
+ if (_this.initConfig.showLog) {
2471
+ _this.printLog("\u4FDD\u5B58\u5F85\u53D1\u9001\u8BF7\u6C42\u5230 LocalStorage \u5931\u8D25: " + e);
2472
+ }
2039
2473
  } // 使用 sendBeacon 发送数据(最可靠的方式)
2040
2474
 
2041
2475
 
2042
2476
  if (navigator.sendBeacon && _this.initConfig.serverUrl) {
2043
2477
  try {
2044
2478
  // 如果只有一条数据,直接发送;否则批量发送
2045
- var dataToSend = allPendingData.length === 1 ? allPendingData[0] : {
2046
- events: allPendingData
2047
- };
2479
+ var dataToSend = allPendingData.length === 1 ? allPendingData[0] : allPendingData;
2048
2480
  var blob = new Blob([JSON.stringify(dataToSend)], {
2049
2481
  type: _this.initConfig.contentType || "application/json"
2050
2482
  });
2051
2483
  var sent = navigator.sendBeacon(_this.initConfig.serverUrl, blob);
2052
2484
 
2053
2485
  if (sent) {
2054
- // 发送成功,清除所有队列
2055
- _this.batchQueue = [];
2056
- _this.pendingRequests = [];
2057
-
2486
+ // 发送成功,清除所有 LocalStorage
2058
2487
  _this.setLocalStorage(_this.BATCH_QUEUE_STORAGE_KEY, "[]");
2059
2488
 
2489
+ _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, "[]");
2490
+
2060
2491
  if (_this.initConfig.showLog) {
2061
2492
  _this.printLog("\u9875\u9762\u5378\u8F7D\u65F6\u6210\u529F\u53D1\u9001 " + allPendingData.length + " \u6761\u6570\u636E");
2062
2493
  }
2063
2494
  } else {
2064
- // sendBeacon 返回 false,保存到 LocalStorage(批量模式)
2065
- if (_this.initConfig.batchSend && _this.batchQueue.length > 0) {
2066
- _this.saveBatchQueueToStorage();
2067
- } // 保存 pendingRequests 到 LocalStorage(如果支持)
2068
-
2069
-
2070
- if (_this.pendingRequests.length > 0) {
2071
- try {
2072
- _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, JSON.stringify(_this.pendingRequests));
2073
- } catch (e) {
2074
- // LocalStorage 可能已满或不可用
2075
- if (_this.initConfig.showLog) {
2076
- _this.printLog("\u4FDD\u5B58\u5F85\u53D1\u9001\u8BF7\u6C42\u5230 LocalStorage \u5931\u8D25: " + e);
2077
- }
2078
- }
2495
+ // sendBeacon 返回 false,数据已在 LocalStorage 中,等待下次恢复
2496
+ if (_this.initConfig.showLog) {
2497
+ _this.printLog("sendBeacon \u8FD4\u56DE false\uFF0C\u6570\u636E\u5DF2\u4FDD\u5B58\u5230 LocalStorage \u7B49\u5F85\u4E0B\u6B21\u6062\u590D");
2079
2498
  }
2080
2499
  }
2081
2500
  } catch (e) {
2082
- // sendBeacon 失败,保存到 LocalStorage(批量模式)
2083
- if (_this.initConfig.batchSend && _this.batchQueue.length > 0) {
2084
- _this.saveBatchQueueToStorage();
2085
- } // 保存 pendingRequests 到 LocalStorage(如果支持)
2086
-
2087
-
2088
- if (_this.pendingRequests.length > 0) {
2089
- try {
2090
- _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, JSON.stringify(_this.pendingRequests));
2091
- } catch (e) {// LocalStorage 可能已满或不可用
2092
- }
2093
- }
2094
-
2501
+ // sendBeacon 失败,数据已在 LocalStorage 中,等待下次恢复
2095
2502
  if (_this.initConfig.showLog) {
2096
- _this.printLog("\u9875\u9762\u5378\u8F7D\u65F6\u53D1\u9001\u6570\u636E\u5931\u8D25: " + e);
2503
+ _this.printLog("\u9875\u9762\u5378\u8F7D\u65F6\u53D1\u9001\u6570\u636E\u5931\u8D25: " + e + "\uFF0C\u6570\u636E\u5DF2\u4FDD\u5B58\u5230 LocalStorage");
2097
2504
  }
2505
+ } finally {
2506
+ // 重置标记
2507
+ _this.isFlushingPendingData = false;
2098
2508
  }
2099
2509
  } else {
2100
- // 不支持 sendBeacon,保存到 LocalStorage(批量模式)
2101
- if (_this.initConfig.batchSend && _this.batchQueue.length > 0) {
2102
- _this.saveBatchQueueToStorage();
2103
- } // 保存 pendingRequests 到 LocalStorage(如果支持)
2510
+ // 不支持 sendBeacon,数据已在 LocalStorage 中,等待下次恢复
2511
+ if (_this.initConfig.showLog) {
2512
+ _this.printLog("\u4E0D\u652F\u6301 sendBeacon\uFF0C\u6570\u636E\u5DF2\u4FDD\u5B58\u5230 LocalStorage \u7B49\u5F85\u4E0B\u6B21\u6062\u590D");
2513
+ } // 重置标记
2104
2514
 
2105
2515
 
2106
- if (_this.pendingRequests.length > 0) {
2107
- try {
2108
- _this.setLocalStorage(_this.PENDING_REQUESTS_STORAGE_KEY, JSON.stringify(_this.pendingRequests));
2109
- } catch (e) {// LocalStorage 可能已满或不可用
2110
- }
2111
- }
2516
+ _this.isFlushingPendingData = false;
2112
2517
  }
2113
2518
  };
2114
2519
  /**
@@ -2131,7 +2536,8 @@ function (_super) {
2131
2536
  contentType = _a.contentType,
2132
2537
  showLog = _a.showLog,
2133
2538
  initHeader = _a.header,
2134
- batchSend = _a.batchSend;
2539
+ batchSend = _a.batchSend,
2540
+ sendMethod = _a.sendMethod;
2135
2541
  if (!!showLog) _this.printLog(params); // 如果启用批量发送
2136
2542
 
2137
2543
  if (batchSend) {
@@ -2141,10 +2547,13 @@ function (_super) {
2141
2547
  success: true,
2142
2548
  message: "已添加到批量队列"
2143
2549
  });
2144
- } // 如果使用 sendBeacon 且没有自定义 header
2550
+ } // 判断是否使用 sendBeacon
2551
+
2552
+
2553
+ var shouldUseBeacon = _this.shouldUseBeacon(sendMethod, header, initHeader); // 如果使用 sendBeacon
2145
2554
 
2146
2555
 
2147
- if (_this.isSupportBeaconSend() === true && !header && !initHeader) {
2556
+ if (shouldUseBeacon) {
2148
2557
  // 检查页面是否即将卸载,如果是,直接使用 sendBeacon 发送,避免被取消
2149
2558
  if (_this.isPageUnloading()) {
2150
2559
  var sent = _this.sendWithBeacon(params, serverUrl, contentType);
@@ -2182,8 +2591,8 @@ function (_super) {
2182
2591
  } else {
2183
2592
  // 使用 XMLHttpRequest 发送
2184
2593
  return new Promise(function (resolve, reject) {
2185
- // 如果页面即将卸载,也尝试使用 sendBeacon 作为备用
2186
- if (_this.isPageUnloading() && _this.isSupportBeaconSend() && !header && !initHeader) {
2594
+ // 如果页面即将卸载且配置为 auto,尝试使用 sendBeacon 作为备用
2595
+ if (_this.isPageUnloading() && sendMethod === 'auto' && _this.isSupportBeaconSend() && !header && !initHeader) {
2187
2596
  var sent = _this.sendWithBeacon(params, serverUrl, contentType);
2188
2597
 
2189
2598
  if (sent) {
@@ -2212,8 +2621,8 @@ function (_super) {
2212
2621
  });
2213
2622
  },
2214
2623
  error: function error(err, status) {
2215
- // 如果请求失败且页面即将卸载,尝试使用 sendBeacon
2216
- if (_this.isPageUnloading() && _this.isSupportBeaconSend() && !header && !initHeader) {
2624
+ // 如果请求失败且页面即将卸载且配置为 auto,尝试使用 sendBeacon
2625
+ if (_this.isPageUnloading() && sendMethod === 'auto' && _this.isSupportBeaconSend() && !header && !initHeader) {
2217
2626
  var sent = _this.sendWithBeacon(params, serverUrl, contentType);
2218
2627
 
2219
2628
  if (sent) {
@@ -2235,6 +2644,30 @@ function (_super) {
2235
2644
  });
2236
2645
  }
2237
2646
  };
2647
+ /**
2648
+ * @description 判断是否应该使用 sendBeacon
2649
+ * @param sendMethod 配置的发送方式
2650
+ * @param header 自定义 header
2651
+ * @param initHeader 初始化配置的 header
2652
+ * @returns 是否使用 sendBeacon
2653
+ */
2654
+
2655
+
2656
+ _this.shouldUseBeacon = function (sendMethod, header, initHeader) {
2657
+ // 如果配置为 xhr,不使用 beacon
2658
+ if (sendMethod === 'xhr') {
2659
+ return false;
2660
+ } // 如果配置为 beacon,检查是否支持
2661
+
2662
+
2663
+ if (sendMethod === 'beacon') {
2664
+ return _this.isSupportBeaconSend() === true;
2665
+ } // 如果配置为 auto(默认),使用原有逻辑
2666
+ // 只有在支持 sendBeacon 且没有自定义 header 时才使用
2667
+
2668
+
2669
+ return _this.isSupportBeaconSend() === true && !header && !initHeader;
2670
+ };
2238
2671
  /**
2239
2672
  * @description 留存时长上报
2240
2673
  * @param type
@@ -2263,6 +2696,121 @@ function (_super) {
2263
2696
  _this.setCookie("retainedStartTime", _this.getTimeStamp());
2264
2697
  }
2265
2698
  };
2699
+ /**
2700
+ * @description 用户主动上报页面停留时长
2701
+ * @param duration 自定义停留时长(毫秒),如果不传则自动计算从页面加载(或上次调用)到当前的时长
2702
+ * @param options 可选参数,包括自定义描述、业务参数等
2703
+ * @param resetStartTime 是否重置起始时间,默认 true(手动上报后重置,定时上报不重置)
2704
+ * @returns Promise<TrackingResponse> 上报结果
2705
+ */
2706
+
2707
+
2708
+ _this.trackPageDuration = function (duration, options, resetStartTime) {
2709
+ if (resetStartTime === void 0) {
2710
+ resetStartTime = true;
2711
+ } // 计算停留时长
2712
+
2713
+
2714
+ var retainedDuration;
2715
+
2716
+ if (duration !== undefined && duration !== null) {
2717
+ // 使用自定义时长
2718
+ retainedDuration = Math.max(duration, 0);
2719
+ } else {
2720
+ // 自动计算时长
2721
+ var __time = _this.getCookie("retainedStartTime");
2722
+
2723
+ var retainedStartTime = __time ? +__time : _this.getTimeStamp();
2724
+
2725
+ var currentTime = _this.getTimeStamp();
2726
+
2727
+ retainedDuration = Math.max(currentTime - retainedStartTime, 0);
2728
+ } // 构建参数
2729
+
2730
+
2731
+ var desc = (options === null || options === void 0 ? void 0 : options.desc) || _this.eventDescMap["PageRetained"];
2732
+ var pageKey = (options === null || options === void 0 ? void 0 : options.pageKey) || _this.pageKey;
2733
+ var business = (options === null || options === void 0 ? void 0 : options.business) || {};
2734
+ var header = options === null || options === void 0 ? void 0 : options.header;
2735
+
2736
+ var params = _this.getParams({
2737
+ event: "PageRetained",
2738
+ desc: desc,
2739
+ itemKey: _this.getItemKey(undefined, pageKey),
2740
+ privateParamMap: {
2741
+ business: business,
2742
+ retainedDuration: retainedDuration
2743
+ }
2744
+ }); // 上报数据
2745
+
2746
+
2747
+ var result = _this.sendData(params, header); // 根据 resetStartTime 参数决定是否重置起始时间
2748
+ // 手动上报后重置起始时间,定时上报不重置(累积计算)
2749
+
2750
+
2751
+ if (resetStartTime) {
2752
+ _this.setCookie("retainedStartTime", _this.getTimeStamp());
2753
+ }
2754
+
2755
+ return result;
2756
+ };
2757
+ /**
2758
+ * @description 启动定时上报页面停留时长的定时器
2759
+ */
2760
+
2761
+
2762
+ _this.startPageDurationTimer = function () {
2763
+ // 先停止现有的定时器(避免重复启动)
2764
+ _this.stopPageDurationTimer();
2765
+
2766
+ var interval = _this.initConfig.pageDurationInterval || 30000; // 检查间隔时间是否有效
2767
+
2768
+ if (interval <= 0) {
2769
+ if (_this.initConfig.showLog) {
2770
+ _this.printLog("定时上报间隔时间无效,已禁用定时上报");
2771
+ }
2772
+
2773
+ return;
2774
+ } // 启动定时器
2775
+
2776
+
2777
+ _this.pageDurationTimer = window.setInterval(function () {
2778
+ // 只在页面可见时上报(避免后台上报)
2779
+ if (document.visibilityState === "visible") {
2780
+ // 定时上报:retainedDuration 直接使用上报间隔时间,数据工程师会清洗数据
2781
+ _this.trackPageDuration(interval, {
2782
+ desc: "定时上报页面停留时长",
2783
+ business: {
2784
+ reportType: "interval",
2785
+ interval: interval
2786
+ }
2787
+ }, true).catch(function (err) {
2788
+ if (_this.initConfig.showLog) {
2789
+ _this.printLog("\u5B9A\u65F6\u4E0A\u62A5\u9875\u9762\u505C\u7559\u65F6\u957F\u5931\u8D25: " + err);
2790
+ }
2791
+ });
2792
+ }
2793
+ }, interval);
2794
+
2795
+ if (_this.initConfig.showLog) {
2796
+ _this.printLog("\u5B9A\u65F6\u4E0A\u62A5\u9875\u9762\u505C\u7559\u65F6\u957F\u5DF2\u542F\u52A8\uFF0C\u95F4\u9694: " + interval + "ms");
2797
+ }
2798
+ };
2799
+ /**
2800
+ * @description 停止定时上报页面停留时长的定时器
2801
+ */
2802
+
2803
+
2804
+ _this.stopPageDurationTimer = function () {
2805
+ if (_this.pageDurationTimer !== null) {
2806
+ clearInterval(_this.pageDurationTimer);
2807
+ _this.pageDurationTimer = null;
2808
+
2809
+ if (_this.initConfig.showLog) {
2810
+ _this.printLog("定时上报页面停留时长已停止");
2811
+ }
2812
+ }
2813
+ };
2266
2814
  /**
2267
2815
  * @description 获取 itemKey
2268
2816
  * @param {[string]} partkey [控件/自定义事件的唯一标识]
@@ -2279,8 +2827,216 @@ function (_super) {
2279
2827
  return str + ("" + (str.length ? "." : "")) + key;
2280
2828
  }, "");
2281
2829
  };
2830
+ /**
2831
+ * @description 从元素或其祖先节点提取 data-* 属性
2832
+ * @param element 目标元素
2833
+ * @returns 提取的业务参数对象
2834
+ */
2835
+
2836
+
2837
+ _this.extractDataAttributes = function (element) {
2838
+ var business = {};
2839
+ var currentElement = element;
2840
+
2841
+ while (currentElement) {
2842
+ var attributes = currentElement.attributes;
2843
+
2844
+ for (var i = 0; i < attributes.length; i++) {
2845
+ var attr = attributes[i];
2846
+ var name_1 = attr.name;
2847
+
2848
+ if (name_1.startsWith('data-') && name_1 !== 'data-exposure' && name_1 !== 'data-part-key' && name_1 !== 'data-desc') {
2849
+ var value = attr.value;
2850
+
2851
+ if (value) {
2852
+ var camelCaseKey = name_1.replace(/^data-/, '').split('-').map(function (part, index) {
2853
+ return index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1);
2854
+ }).join('');
2855
+ business[camelCaseKey] = value;
2856
+ }
2857
+ }
2858
+ }
2859
+
2860
+ currentElement = currentElement.parentElement;
2861
+
2862
+ if (currentElement && currentElement.tagName === 'BODY') {
2863
+ break;
2864
+ }
2865
+ }
2866
+
2867
+ return business;
2868
+ };
2869
+ /**
2870
+ * @description 初始化曝光监听
2871
+ */
2872
+
2873
+
2874
+ _this.initExposureObserver = function () {
2875
+ if (!_this.initConfig.autoTrackExposure) {
2876
+ return;
2877
+ }
2878
+
2879
+ if (!('IntersectionObserver' in window)) {
2880
+ if (_this.initConfig.showLog) {
2881
+ _this.printLog('当前浏览器不支持 IntersectionObserver,无法启用曝光埋点');
2882
+ }
2883
+
2884
+ return;
2885
+ }
2886
+
2887
+ var threshold = _this.initConfig.exposureThreshold || 0.5;
2888
+ _this.exposureObserver = new IntersectionObserver(function (entries) {
2889
+ entries.forEach(function (entry) {
2890
+ var element = entry.target;
2891
+
2892
+ var elementInfo = _this.exposureElementsMap.get(element);
2893
+
2894
+ if (!elementInfo) {
2895
+ return;
2896
+ }
2897
+
2898
+ var exposureTime = _this.initConfig.exposureTime || 500;
2899
+
2900
+ if (entry.isIntersecting) {
2901
+ elementInfo.isVisible = true;
2902
+ elementInfo.visibleStartTime = _this.getTimeStamp();
2903
+
2904
+ if (elementInfo.exposureTimer) {
2905
+ clearTimeout(elementInfo.exposureTimer);
2906
+ }
2907
+
2908
+ elementInfo.exposureTimer = window.setTimeout(function () {
2909
+ if (elementInfo.isVisible) {
2910
+ _this.reportExposure(element);
2911
+ }
2912
+ }, exposureTime);
2913
+ } else {
2914
+ elementInfo.isVisible = false;
2915
+
2916
+ if (elementInfo.exposureTimer) {
2917
+ clearTimeout(elementInfo.exposureTimer);
2918
+ elementInfo.exposureTimer = null;
2919
+ }
2920
+ }
2921
+ });
2922
+ }, {
2923
+ threshold: threshold
2924
+ });
2925
+
2926
+ _this.observeExposureElements();
2927
+ };
2928
+ /**
2929
+ * @description 监听页面上的曝光元素
2930
+ */
2931
+
2932
+
2933
+ _this.observeExposureElements = function () {
2934
+ if (!_this.exposureObserver) {
2935
+ return;
2936
+ }
2937
+
2938
+ var elements = document.querySelectorAll('[data-exposure="true"]');
2939
+ elements.forEach(function (element) {
2940
+ if (!_this.exposureElementsMap.has(element)) {
2941
+ var htmlElement = element;
2942
+
2943
+ _this.exposureElementsMap.set(htmlElement, {
2944
+ element: htmlElement,
2945
+ visibleStartTime: 0,
2946
+ exposureCount: 0,
2947
+ isVisible: false,
2948
+ exposureTimer: null
2949
+ });
2950
+
2951
+ _this.exposureObserver.observe(htmlElement);
2952
+ }
2953
+ });
2954
+
2955
+ if (_this.initConfig.showLog && elements.length > 0) {
2956
+ _this.printLog("\u5DF2\u76D1\u542C " + elements.length + " \u4E2A\u66DD\u5149\u5143\u7D20");
2957
+ }
2958
+ };
2959
+ /**
2960
+ * @description 上报曝光事件
2961
+ * @param element 曝光元素
2962
+ */
2963
+
2964
+
2965
+ _this.reportExposure = function (element) {
2966
+ var elementInfo = _this.exposureElementsMap.get(element);
2967
+
2968
+ if (!elementInfo) {
2969
+ return;
2970
+ }
2971
+
2972
+ var exposureNum = _this.initConfig.exposureNum;
2973
+
2974
+ if (exposureNum !== undefined && elementInfo.exposureCount >= exposureNum) {
2975
+ if (_this.initConfig.showLog) {
2976
+ _this.printLog("\u5143\u7D20\u5DF2\u8FBE\u5230\u6700\u5927\u66DD\u5149\u6B21\u6570\u9650\u5236: " + exposureNum);
2977
+ }
2978
+
2979
+ return;
2980
+ }
2981
+
2982
+ var business = _this.extractDataAttributes(element);
2983
+
2984
+ var desc = element.getAttribute('data-desc') || _this.eventDescMap['WebExposure'];
2985
+
2986
+ var partkey = element.getAttribute('data-part-key') || 'exposure';
2987
+
2988
+ var params = _this.getParams({
2989
+ event: 'WebExposure',
2990
+ desc: desc,
2991
+ itemKey: _this.getItemKey(partkey),
2992
+ privateParamMap: {
2993
+ business: business
2994
+ }
2995
+ });
2996
+
2997
+ _this.sendData(params).then(function () {
2998
+ elementInfo.exposureCount++;
2999
+
3000
+ if (elementInfo.exposureTimer) {
3001
+ clearTimeout(elementInfo.exposureTimer);
3002
+ elementInfo.exposureTimer = null;
3003
+ }
3004
+
3005
+ if (_this.initConfig.showLog) {
3006
+ _this.printLog("\u66DD\u5149\u4E0A\u62A5\u6210\u529F\uFF0C\u5F53\u524D\u66DD\u5149\u6B21\u6570: " + elementInfo.exposureCount);
3007
+ }
3008
+ }).catch(function (err) {
3009
+ if (_this.initConfig.showLog) {
3010
+ _this.printLog("\u66DD\u5149\u4E0A\u62A5\u5931\u8D25: " + err);
3011
+ }
3012
+ });
3013
+ };
3014
+ /**
3015
+ * @description 停止曝光监听
3016
+ */
3017
+
3018
+
3019
+ _this.stopExposureObserver = function () {
3020
+ if (_this.exposureObserver) {
3021
+ _this.exposureObserver.disconnect();
3022
+
3023
+ _this.exposureObserver = null;
3024
+
3025
+ _this.exposureElementsMap.forEach(function (elementInfo) {
3026
+ if (elementInfo.exposureTimer) {
3027
+ clearTimeout(elementInfo.exposureTimer);
3028
+ }
3029
+ });
3030
+
3031
+ _this.exposureElementsMap.clear();
3032
+
3033
+ if (_this.initConfig.showLog) {
3034
+ _this.printLog('曝光监听已停止');
3035
+ }
3036
+ }
3037
+ };
2282
3038
 
2283
- _this.sdkVersion = "1.2.0"; // sdk版本
3039
+ _this.sdkVersion = "1.2.4"; // sdk版本
2284
3040
 
2285
3041
  _this.initConfig = {
2286
3042
  appKey: "",
@@ -2298,7 +3054,14 @@ function (_super) {
2298
3054
  batchInterval: 5000,
2299
3055
  batchMaxSize: 10,
2300
3056
  trackPartKeyClick: false,
2301
- pendingRequestsMaxSize: 50 // 待发送请求队列最大数量(防止内存溢出)
3057
+ pendingRequestsMaxSize: 50,
3058
+ autoTrackPageDurationInterval: false,
3059
+ pageDurationInterval: 30000,
3060
+ sendMethod: "auto",
3061
+ autoTrackExposure: false,
3062
+ exposureThreshold: 0.5,
3063
+ exposureTime: 500,
3064
+ exposureNum: undefined // 同一元素允许上报的最大曝光次数,不限制
2302
3065
 
2303
3066
  }; // 系统信息
2304
3067