@fle-sdk/event-tracking-web 1.2.3 → 1.2.5

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  > **构建用户数据体系,让用户行为数据发挥深远的价值。**
2
2
 
3
- **当前版本**: v1.2.3
3
+ **当前版本**: v1.2.4
4
4
 
5
5
  ## 前言
6
6
 
@@ -209,6 +209,10 @@ WebTracking
209
209
  | sendMethod | string | 数据发送方式:auto(自动选择)、xhr(XMLHttpRequest)、beacon(sendBeacon) | 否 | auto |
210
210
  | autoTrackPageDurationInterval | boolean | 是否定时上报页面停留时长 | 否 | false |
211
211
  | pageDurationInterval | number | 定时上报页面停留时长的间隔时间(毫秒) | 否 | 30000 |
212
+ | autoTrackExposure | boolean | 是否启用曝光埋点 | 否 | false |
213
+ | exposureThreshold | number | 曝光进入可视区判定阈值(0-1之间) | 否 | 0.5 |
214
+ | exposureTime | number | 曝光可见停留时长阈值(毫秒) | 否 | 500 |
215
+ | exposureNum | number | 同一元素允许上报的最大曝光次数(不传则不限制) | 否 | 不限制 |
212
216
  | pageKey | string | 自定义页面唯一标识,如果不传则自动从路由获取 | 否 | 自动生成 |
213
217
 
214
218
  #### 例子
@@ -276,6 +280,10 @@ export default App;
276
280
  | sendMethod | string | 数据发送方式:auto(自动选择)、xhr(XMLHttpRequest)、beacon(sendBeacon) | 否 | auto |
277
281
  | autoTrackPageDurationInterval | boolean | 是否定时上报页面停留时长 | 否 | false |
278
282
  | pageDurationInterval | number | 定时上报页面停留时长的间隔时间(毫秒) | 否 | 30000 |
283
+ | autoTrackExposure | boolean | 是否启用曝光埋点 | 否 | false |
284
+ | exposureThreshold | number | 曝光进入可视区判定阈值(0-1之间) | 否 | 0.5 |
285
+ | exposureTime | number | 曝光可见停留时长阈值(毫秒) | 否 | 500 |
286
+ | exposureNum | number | 同一元素允许上报的最大曝光次数(不传则不限制) | 否 | 不限制 |
279
287
  | pageKey | string | 自定义页面唯一标识,如果不传则自动从路由获取 | 否 | 自动生成 |
280
288
 
281
289
  #### 例子
@@ -969,11 +977,123 @@ WebTracking.init({
969
977
 
970
978
  两种方式可以同时使用,互不冲突。
971
979
 
980
+ ### 5.8 元素曝光埋点
981
+
982
+ SDK 支持自动监听元素曝光事件,适用于需要追踪元素可见性的场景(如商品卡片曝光、广告曝光等)。
983
+
984
+ #### 触发条件
985
+
986
+ 仅对包含 `data-exposure="true"` 的 DOM 元素做曝光监听与上报(区分大小写,建议统一为字符串 "true")。
987
+
988
+ #### 配置参数
989
+
990
+ - `autoTrackExposure`: 是否启用曝光埋点(默认:false)
991
+ - `exposureThreshold`: 曝光进入可视区判定阈值,0-1之间(默认:0.5)
992
+ - `exposureTime`: 曝光可见停留时长阈值,单位毫秒(默认:500)
993
+ - `exposureNum`: 同一元素允许上报的最大曝光次数(默认:不限制)
994
+
995
+ #### 工作原理
996
+
997
+ 1. 使用 `IntersectionObserver` API 判定元素是否进入可视区
998
+ 2. 元素进入可视区后开始计时
999
+ 3. 元素离开可视区时,如果可见时长达到 `exposureTime` 阈值,则上报曝光事件
1000
+ 4. 如果设置了 `exposureNum`,同一元素超过该次数后不再上报
1001
+
1002
+ #### 上报参数提取
1003
+
1004
+ 从目标元素(或其祖先节点,按"就近原则")提取所有 `data-*` 属性作为埋点业务参数,规则如下:
1005
+
1006
+ - 属性名从 `data-xxx-yyy` 转为 `xxxYyy`(camelCase)
1007
+ - 示例:`data-goods-id` → `goodsId`,`data-list-id` → `listId`
1008
+ - 排除属性:`data-exposure`、`data-part-key`、`data-desc`
1009
+
1010
+ #### 使用示例
1011
+
1012
+ ```jsx
1013
+ WebTracking.init({
1014
+ appKey: "218844",
1015
+ serverUrl: "https://xxx/push",
1016
+ // 启用曝光埋点
1017
+ autoTrackExposure: true,
1018
+ // 元素进入可视区 50% 时判定为可见
1019
+ exposureThreshold: 0.5,
1020
+ // 元素连续可见 500ms 才算一次有效曝光
1021
+ exposureTime: 500,
1022
+ // 同一元素最多上报 3 次曝光
1023
+ exposureNum: 3,
1024
+ });
1025
+ ```
1026
+
1027
+ #### HTML 使用示例
1028
+
1029
+ ```jsx
1030
+ <div className="goods-list">
1031
+ <div
1032
+ data-exposure="true"
1033
+ data-part-key="goods_card"
1034
+ data-desc="商品卡片曝光"
1035
+ data-goods-id="12345"
1036
+ data-goods-name="测试商品"
1037
+ data-list-id="home_list"
1038
+ >
1039
+ <img src="goods.jpg" alt="商品图片" />
1040
+ <h3>商品名称</h3>
1041
+ </div>
1042
+
1043
+ <div
1044
+ data-exposure="true"
1045
+ data-part-key="ad_banner"
1046
+ data-desc="广告横幅曝光"
1047
+ data-ad-id="999"
1048
+ data-ad-position="top"
1049
+ >
1050
+ <img src="ad.jpg" alt="广告图片" />
1051
+ </div>
1052
+ </div>
1053
+ ```
1054
+
1055
+ #### 上报数据示例
1056
+
1057
+ ```json
1058
+ {
1059
+ "event": "WebExposure",
1060
+ "desc": "商品卡片曝光",
1061
+ "itemKey": "218844.home_page.goods_card",
1062
+ "requestTime": 1709524231171,
1063
+ "deviceId": "1dd539cdea9332ebb9d5c087f9d4471f",
1064
+ "privateParamMap": {
1065
+ "business": {
1066
+ "goodsId": "12345",
1067
+ "goodsName": "测试商品",
1068
+ "listId": "home_list"
1069
+ }
1070
+ }
1071
+ }
1072
+ ```
1073
+
1074
+ #### 注意事项
1075
+
1076
+ 1. **浏览器兼容性**:需要浏览器支持 `IntersectionObserver` 和 `MutationObserver` API,不支持时会自动禁用曝光埋点
1077
+ 2. **性能优化**:SDK 会自动去重,避免重复监听同一元素
1078
+ 3. **动态元素**:自动监听动态添加的曝光元素,无需手动调用
1079
+ 4. **数据提取**:`data-*` 属性会从目标元素向上遍历到 BODY 标签,按"就近原则"提取
1080
+
972
1081
  ---
973
1082
 
974
1083
  ## 六、版本更新日志
975
1084
 
976
- ### v1.2.3 (最新)
1085
+ ### v1.2.4 (最新)
1086
+
1087
+ - ✨ 新增元素曝光埋点功能
1088
+ - `autoTrackExposure` - 是否启用曝光埋点
1089
+ - `exposureThreshold` - 曝光进入可视区判定阈值(0-1之间)
1090
+ - `exposureTime` - 曝光可见停留时长阈值(毫秒)
1091
+ - `exposureNum` - 同一元素允许上报的最大曝光次数
1092
+ - 使用 `IntersectionObserver` API 判定元素可见性
1093
+ - 自动提取 `data-*` 属性作为业务参数
1094
+ - 支持自定义曝光次数限制
1095
+
1096
+ ### v1.2.3
977
1097
 
978
1098
  - 🐛 修复批量发送数据格式问题
979
1099
  - 批量发送时直接发送数组,不再包装在 `events` 参数中
package/lib/index.esm.js CHANGED
@@ -1253,7 +1253,13 @@ function (_super) {
1253
1253
 
1254
1254
  _this.DEFAULT_PENDING_REQUESTS_MAX_SIZE = 50; // LocalStorage 最大大小限制(4MB)
1255
1255
 
1256
- _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(); // MutationObserver 实例(用于监听动态添加的元素)
1261
+
1262
+ _this.mutationObserver = null; // 用户信息
1257
1263
 
1258
1264
  _this.userInfo = null; // 当前路由
1259
1265
 
@@ -1267,7 +1273,8 @@ function (_super) {
1267
1273
  PageView: "Web 浏览页面",
1268
1274
  WebClick: "Web 元素点击",
1269
1275
  PageRetained: "Web 页面浏览时长",
1270
- CustomTrack: "Web 自定义代码上报"
1276
+ CustomTrack: "Web 自定义代码上报",
1277
+ WebExposure: "Web 元素曝光"
1271
1278
  };
1272
1279
  /**
1273
1280
  * @description 初始化函数
@@ -1312,6 +1319,11 @@ function (_super) {
1312
1319
 
1313
1320
  if (_this.initConfig.autoTrackPageDurationInterval) {
1314
1321
  _this.startPageDurationTimer();
1322
+ } // 如果启用了曝光监听,初始化曝光监听
1323
+
1324
+
1325
+ if (_this.initConfig.autoTrackExposure) {
1326
+ _this.initExposureObserver();
1315
1327
  }
1316
1328
  };
1317
1329
  /**
@@ -1372,6 +1384,13 @@ function (_super) {
1372
1384
  _this.startPageDurationTimer();
1373
1385
  } else {
1374
1386
  _this.stopPageDurationTimer();
1387
+ } // 处理曝光监听配置
1388
+
1389
+
1390
+ if (_this.initConfig.autoTrackExposure) {
1391
+ _this.initExposureObserver();
1392
+ } else {
1393
+ _this.stopExposureObserver();
1375
1394
  }
1376
1395
  };
1377
1396
  /**
@@ -1454,6 +1473,36 @@ function (_super) {
1454
1473
 
1455
1474
  break;
1456
1475
 
1476
+ case 'exposureThreshold':
1477
+ if (typeof value !== 'number' || value < 0 || value > 1) {
1478
+ return {
1479
+ valid: false,
1480
+ message: 'exposureThreshold 必须是 0-1 之间的数字'
1481
+ };
1482
+ }
1483
+
1484
+ break;
1485
+
1486
+ case 'exposureTime':
1487
+ if (typeof value !== 'number' || value <= 0) {
1488
+ return {
1489
+ valid: false,
1490
+ message: 'exposureTime 必须是大于 0 的数字'
1491
+ };
1492
+ }
1493
+
1494
+ break;
1495
+
1496
+ case 'exposureNum':
1497
+ if (value !== undefined && (typeof value !== 'number' || value <= 0 || !Number.isInteger(value))) {
1498
+ return {
1499
+ valid: false,
1500
+ message: 'exposureNum 必须是大于 0 的整数或不限制'
1501
+ };
1502
+ }
1503
+
1504
+ break;
1505
+
1457
1506
  case 'showLog':
1458
1507
  case 'autoTrack':
1459
1508
  case 'isTrackSinglePage':
@@ -2780,8 +2829,275 @@ function (_super) {
2780
2829
  return str + ("" + (str.length ? "." : "")) + key;
2781
2830
  }, "");
2782
2831
  };
2832
+ /**
2833
+ * @description 从元素或其祖先节点提取 data-* 属性
2834
+ * @param element 目标元素
2835
+ * @returns 提取的业务参数对象
2836
+ */
2837
+
2838
+
2839
+ _this.extractDataAttributes = function (element) {
2840
+ var business = {};
2841
+ var currentElement = element;
2842
+
2843
+ while (currentElement) {
2844
+ var attributes = currentElement.attributes;
2845
+
2846
+ for (var i = 0; i < attributes.length; i++) {
2847
+ var attr = attributes[i];
2848
+ var name_1 = attr.name;
2849
+
2850
+ if (name_1.startsWith('data-') && name_1 !== 'data-exposure' && name_1 !== 'data-part-key' && name_1 !== 'data-desc') {
2851
+ var value = attr.value;
2852
+
2853
+ if (value) {
2854
+ var camelCaseKey = name_1.replace(/^data-/, '').split('-').map(function (part, index) {
2855
+ return index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1);
2856
+ }).join('');
2857
+ business[camelCaseKey] = value;
2858
+ }
2859
+ }
2860
+ }
2861
+
2862
+ currentElement = currentElement.parentElement;
2863
+
2864
+ if (currentElement && currentElement.tagName === 'BODY') {
2865
+ break;
2866
+ }
2867
+ }
2868
+
2869
+ return business;
2870
+ };
2871
+ /**
2872
+ * @description 初始化曝光监听
2873
+ */
2874
+
2875
+
2876
+ _this.initExposureObserver = function () {
2877
+ if (!_this.initConfig.autoTrackExposure) {
2878
+ return;
2879
+ }
2880
+
2881
+ if (!('IntersectionObserver' in window)) {
2882
+ if (_this.initConfig.showLog) {
2883
+ _this.printLog('当前浏览器不支持 IntersectionObserver,无法启用曝光埋点');
2884
+ }
2885
+
2886
+ return;
2887
+ }
2888
+
2889
+ var threshold = _this.initConfig.exposureThreshold || 0.5;
2890
+ _this.exposureObserver = new IntersectionObserver(function (entries) {
2891
+ entries.forEach(function (entry) {
2892
+ var element = entry.target;
2893
+
2894
+ var elementInfo = _this.exposureElementsMap.get(element);
2895
+
2896
+ if (!elementInfo) {
2897
+ return;
2898
+ }
2899
+
2900
+ var exposureTime = _this.initConfig.exposureTime || 500;
2901
+
2902
+ if (entry.isIntersecting) {
2903
+ elementInfo.isVisible = true;
2904
+ elementInfo.visibleStartTime = _this.getTimeStamp();
2905
+
2906
+ if (elementInfo.exposureTimer) {
2907
+ clearTimeout(elementInfo.exposureTimer);
2908
+ }
2909
+
2910
+ elementInfo.exposureTimer = window.setTimeout(function () {
2911
+ if (elementInfo.isVisible) {
2912
+ _this.reportExposure(element);
2913
+ }
2914
+ }, exposureTime);
2915
+ } else {
2916
+ elementInfo.isVisible = false;
2917
+
2918
+ if (elementInfo.exposureTimer) {
2919
+ clearTimeout(elementInfo.exposureTimer);
2920
+ elementInfo.exposureTimer = null;
2921
+ }
2922
+ }
2923
+ });
2924
+ }, {
2925
+ threshold: threshold
2926
+ });
2927
+
2928
+ _this.observeExposureElements();
2929
+
2930
+ _this.initMutationObserver();
2931
+ };
2932
+ /**
2933
+ * @description 添加单个曝光元素到监听
2934
+ * @param element 曝光元素
2935
+ */
2936
+
2937
+
2938
+ _this.addExposureElement = function (element) {
2939
+ if (!_this.exposureElementsMap.has(element)) {
2940
+ _this.exposureElementsMap.set(element, {
2941
+ element: element,
2942
+ visibleStartTime: 0,
2943
+ exposureCount: 0,
2944
+ isVisible: false,
2945
+ exposureTimer: null
2946
+ });
2947
+
2948
+ if (_this.exposureObserver) {
2949
+ _this.exposureObserver.observe(element);
2950
+ }
2951
+ }
2952
+ };
2953
+ /**
2954
+ * @description 监听页面上的曝光元素
2955
+ */
2956
+
2957
+
2958
+ _this.observeExposureElements = function () {
2959
+ if (!_this.exposureObserver) {
2960
+ return;
2961
+ }
2962
+
2963
+ var elements = document.querySelectorAll('[data-exposure="true"]');
2964
+ elements.forEach(function (element) {
2965
+ _this.addExposureElement(element);
2966
+ });
2967
+
2968
+ if (_this.initConfig.showLog && elements.length > 0) {
2969
+ _this.printLog("\u5DF2\u76D1\u542C " + elements.length + " \u4E2A\u66DD\u5149\u5143\u7D20");
2970
+ }
2971
+ };
2972
+ /**
2973
+ * @description 上报曝光事件
2974
+ * @param element 曝光元素
2975
+ */
2976
+
2977
+
2978
+ _this.reportExposure = function (element) {
2979
+ var elementInfo = _this.exposureElementsMap.get(element);
2980
+
2981
+ if (!elementInfo) {
2982
+ return;
2983
+ }
2984
+
2985
+ var exposureNum = _this.initConfig.exposureNum;
2986
+
2987
+ if (exposureNum !== undefined && elementInfo.exposureCount >= exposureNum) {
2988
+ if (_this.initConfig.showLog) {
2989
+ _this.printLog("\u5143\u7D20\u5DF2\u8FBE\u5230\u6700\u5927\u66DD\u5149\u6B21\u6570\u9650\u5236: " + exposureNum);
2990
+ }
2991
+
2992
+ return;
2993
+ }
2994
+
2995
+ var business = _this.extractDataAttributes(element);
2996
+
2997
+ var desc = element.getAttribute('data-desc') || _this.eventDescMap['WebExposure'];
2998
+
2999
+ var partkey = element.getAttribute('data-part-key') || 'exposure';
3000
+
3001
+ var params = _this.getParams({
3002
+ event: 'WebExposure',
3003
+ desc: desc,
3004
+ itemKey: _this.getItemKey(partkey),
3005
+ privateParamMap: {
3006
+ business: business
3007
+ }
3008
+ });
3009
+
3010
+ _this.sendData(params).then(function () {
3011
+ elementInfo.exposureCount++;
3012
+
3013
+ if (elementInfo.exposureTimer) {
3014
+ clearTimeout(elementInfo.exposureTimer);
3015
+ elementInfo.exposureTimer = null;
3016
+ }
3017
+
3018
+ if (_this.initConfig.showLog) {
3019
+ _this.printLog("\u66DD\u5149\u4E0A\u62A5\u6210\u529F\uFF0C\u5F53\u524D\u66DD\u5149\u6B21\u6570: " + elementInfo.exposureCount);
3020
+ }
3021
+ }).catch(function (err) {
3022
+ if (_this.initConfig.showLog) {
3023
+ _this.printLog("\u66DD\u5149\u4E0A\u62A5\u5931\u8D25: " + err);
3024
+ }
3025
+ });
3026
+ };
3027
+ /**
3028
+ * @description 初始化 MutationObserver 监听动态添加的元素
3029
+ */
3030
+
3031
+
3032
+ _this.initMutationObserver = function () {
3033
+ if (!('MutationObserver' in window)) {
3034
+ if (_this.initConfig.showLog) {
3035
+ _this.printLog('当前浏览器不支持 MutationObserver,无法监听动态添加的曝光元素');
3036
+ }
3037
+
3038
+ return;
3039
+ }
3040
+
3041
+ _this.mutationObserver = new MutationObserver(function (mutations) {
3042
+ mutations.forEach(function (mutation) {
3043
+ mutation.addedNodes.forEach(function (node) {
3044
+ if (node.nodeType === Node.ELEMENT_NODE) {
3045
+ var element = node;
3046
+
3047
+ if (element.hasAttribute('data-exposure') && element.getAttribute('data-exposure') === 'true') {
3048
+ _this.addExposureElement(element);
3049
+ } else {
3050
+ var exposureElements = element.querySelectorAll('[data-exposure="true"]');
3051
+ exposureElements.forEach(function (exposureElement) {
3052
+ _this.addExposureElement(exposureElement);
3053
+ });
3054
+ }
3055
+ }
3056
+ });
3057
+ });
3058
+ });
3059
+
3060
+ _this.mutationObserver.observe(document.body, {
3061
+ childList: true,
3062
+ subtree: true
3063
+ });
3064
+
3065
+ if (_this.initConfig.showLog) {
3066
+ _this.printLog('MutationObserver 已启动,监听动态添加的曝光元素');
3067
+ }
3068
+ };
3069
+ /**
3070
+ * @description 停止曝光监听
3071
+ */
3072
+
3073
+
3074
+ _this.stopExposureObserver = function () {
3075
+ if (_this.exposureObserver) {
3076
+ _this.exposureObserver.disconnect();
3077
+
3078
+ _this.exposureObserver = null;
3079
+
3080
+ _this.exposureElementsMap.forEach(function (elementInfo) {
3081
+ if (elementInfo.exposureTimer) {
3082
+ clearTimeout(elementInfo.exposureTimer);
3083
+ }
3084
+ });
3085
+
3086
+ _this.exposureElementsMap.clear();
3087
+ }
3088
+
3089
+ if (_this.mutationObserver) {
3090
+ _this.mutationObserver.disconnect();
3091
+
3092
+ _this.mutationObserver = null;
3093
+ }
3094
+
3095
+ if (_this.initConfig.showLog) {
3096
+ _this.printLog('曝光监听已停止');
3097
+ }
3098
+ };
2783
3099
 
2784
- _this.sdkVersion = "1.2.3"; // sdk版本
3100
+ _this.sdkVersion = "1.2.4"; // sdk版本
2785
3101
 
2786
3102
  _this.initConfig = {
2787
3103
  appKey: "",
@@ -2802,7 +3118,11 @@ function (_super) {
2802
3118
  pendingRequestsMaxSize: 50,
2803
3119
  autoTrackPageDurationInterval: false,
2804
3120
  pageDurationInterval: 30000,
2805
- sendMethod: "auto" // 数据发送方式:auto(自动选择)、xhr(XMLHttpRequest)、beacon(sendBeacon)
3121
+ sendMethod: "auto",
3122
+ autoTrackExposure: false,
3123
+ exposureThreshold: 0.5,
3124
+ exposureTime: 500,
3125
+ exposureNum: undefined // 同一元素允许上报的最大曝光次数,不限制
2806
3126
 
2807
3127
  }; // 系统信息
2808
3128