buriedPiont.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. // 通用 JSBridge
  2. window.JSBridge = {
  3. getOS: function () {
  4. const ua = navigator.userAgent || navigator.vendor || window.opera;
  5. if (/android/i.test(ua)) return 'Android';
  6. if (/iPad|iPhone|iPod/.test(ua) && !window.MSStream) return 'iOS';
  7. return 'Web';
  8. },
  9. getUserId: function (callback) {
  10. // Android WebView
  11. if (window.Android && typeof window.Android.getUserId === 'function') {
  12. callback(window.Android.getUserId());
  13. return;
  14. }
  15. // iOS WebView
  16. if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getUserId) {
  17. window.webkit.messageHandlers.getUserId.postMessage(null);
  18. window.onUserIdReceived = callback; // iOS原生需回调window.onUserIdReceived(userId)
  19. return;
  20. }
  21. // Web环境(用 FingerprintJS 生成指纹)
  22. if (window.FingerprintJS) {
  23. FingerprintJS.load().then(fp => {
  24. fp.get().then(result => {
  25. callback(result.visitorId);
  26. });
  27. });
  28. } else {
  29. callback('unsupported');
  30. }
  31. }
  32. };
  33. let userId = '';
  34. JSBridge.getUserId(function (id) {
  35. userId = id;
  36. });
  37. class Tracker {
  38. constructor(options = {}) {
  39. this.baseUrl = options.baseUrl || '';
  40. this.timer = null;
  41. this.startTime = 0;
  42. this.heartbeatInterval = options.heartbeatInterval || 30000; // 默认30秒发送一次心跳
  43. this.visitCookieTimeout = options.visitCookieTimeout || 1; // 默认访问次数cookie有效期1分钟
  44. this.maxVisitCount = options.maxVisitCount || 5; // 默认最大访问次数5次
  45. this.blockCookieTimeout = options.blockCookieTimeout || 180; // 默认封锁cookie有效期180分钟
  46. this.myEventSend = options.myEventSend || (() => {}); // 是否开启自定义事件上报
  47. this.beforeDestroy = options.beforeDestroy || (() => {}); // 销毁前的回调函数
  48. this.crashTime = options.crashTime || 3; // 闪退阈值
  49. this.idleTimeout = options.idleTimeout || 300000; // 默认 5 分钟无操作视为离开
  50. }
  51. init() {
  52. const visitCookieName = 'userVisitCount';
  53. const blockCookieName = 'userBlocked';
  54. const isBlocked = getCookie(blockCookieName);
  55. const doneFN = () => {
  56. this.startTime = Date.now();
  57. this.bindEvents();
  58. this.startHeartbeat();
  59. }
  60. if (isBlocked) {
  61. // restoreBlock();
  62. return;
  63. }
  64. let visitCount = getCookie(visitCookieName);
  65. if (visitCount) {
  66. visitCount = parseInt(visitCount, 10);
  67. if (visitCount >= this.maxVisitCount) {
  68. setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟
  69. deleteCookie(visitCookieName);
  70. // restoreBlock();
  71. } else {
  72. visitCount += 1;
  73. setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟
  74. doneFN();
  75. }
  76. } else {
  77. setCookie(visitCookieName, '1', this.visitCookieTimeout); // 首次访问,设置访问次数为 1
  78. doneFN();
  79. }
  80. }
  81. bindEvents() {
  82. window.addEventListener('beforeunload', this.handleBeforeUnload);
  83. window.addEventListener('click', this.handleClickEvent);
  84. document.addEventListener('visibilitychange', this.handleVisibilityChange);
  85. window.addEventListener('mousemove', this.resetIdleTimer);
  86. window.addEventListener('keydown', this.resetIdleTimer);
  87. this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报
  88. }
  89. startHeartbeat() {
  90. this.timer = setInterval(() => {
  91. console.log(userId);
  92. this.sendData({
  93. eventType: 'heartbeat',
  94. duration: Math.round((Date.now() - this.startTime) / 1000),
  95. }, { heartbeatSeconds: this.heartbeatInterval, userId: userId });
  96. }, this.heartbeatInterval);
  97. }
  98. // 页面初始化调用,手动在不同框架的生命周期中调用
  99. markContentLoaded() {
  100. this.sendData({
  101. eventType: 'content_loaded',
  102. loadTime: Math.round((Date.now() - this.startTime) / 1000),
  103. });
  104. }
  105. // 用户无操作时发送不活跃事件
  106. resetIdleTimer() {
  107. if (this.idleTimer) {
  108. clearTimeout(this.idleTimer);
  109. }
  110. this.idleTimer = setTimeout(() => {
  111. this.sendData({
  112. eventType: 'user_inactive',
  113. duration: Math.round((Date.now() - this.startTime) / 1000),
  114. });
  115. }, this.idleTimeout);
  116. }
  117. handleBeforeUnload = (e) => {
  118. const duration = Math.round((Date.now() - this.startTime) / 1000);
  119. const data = {
  120. eventType: 'page_close',
  121. duration,
  122. exitType: duration > this.crashTime ? 'crash' : 'normal',
  123. };
  124. const payload = JSON.stringify(data);
  125. if (navigator.sendBeacon) {
  126. navigator.sendBeacon(this.baseUrl, payload);
  127. } else {
  128. this.sendData(payload, { heartbeatSeconds: this.heartbeatInterval, userId: userId });
  129. }
  130. };
  131. handleClickEvent = (event) => {
  132. const target = event.target;
  133. if (target.matches('[data-track]')) {
  134. const trackInfo = {
  135. eventType: 'button_click',
  136. elementId: target.id || '无ID',
  137. elementClass: target.className || '无class',
  138. text: target.innerText || target.textContent || '',
  139. timestamp: new Date().toISOString(),
  140. };
  141. this.sendData(trackInfo, { heartbeatSeconds: this.heartbeatInterval, userId: userId });
  142. }
  143. };
  144. handleVisibilityChange = () => {
  145. if (document.hidden) {
  146. // 用户切换到其他 tab
  147. this.sendData({ eventType: 'tab_inactive' });
  148. } else {
  149. // 用户回到当前 tab
  150. this.sendData({ eventType: 'tab_active' });
  151. }
  152. };
  153. queueIdleTask(task, timeout = 1000) {
  154. if (typeof requestIdleCallback === 'function') {
  155. requestIdleCallback(() => {
  156. task();
  157. }, { timeout });
  158. } else {
  159. // 兜底方案:使用 setTimeout 模拟
  160. setTimeout(task, 0);
  161. }
  162. }
  163. sendData(data, headers = {}) {
  164. this.queueIdleTask(() => {
  165. console.log('上报埋点数据:', data);
  166. fetch(`${this.baseUrl}`, {
  167. method: 'POST',
  168. headers: { 'Content-Type': 'application/json', ...headers },
  169. body: JSON.stringify(data)
  170. }).catch(err => {
  171. console.error('埋点失败:', err);
  172. });
  173. });
  174. }
  175. destroy() {
  176. window.removeEventListener('beforeunload', this.handleBeforeUnload);
  177. window.removeEventListener('click', this.handleClickEvent);
  178. document.removeEventListener('visibilitychange', this.handleVisibilityChange);
  179. window.removeEventListener('mousemove', this.resetIdleTimer);
  180. window.removeEventListener('keydown', this.resetIdleTimer);
  181. this.beforeDestroy();
  182. if (this.timer) {
  183. clearInterval(this.timer);
  184. }
  185. this.sendData({
  186. eventType: 'tracker_destroyed',
  187. duration: Math.round((Date.now() - this.startTime) / 1000),
  188. }, { heartbeatSeconds: this.heartbeatInterval, userId: userId });
  189. }
  190. }
  191. // 使用示例:
  192. // const tracker = new Tracker({ baseUrl: 'https://your-tracking-api.com' });
  193. // tracker.init();
  194. // 暴露给全局或者作为模块导出
  195. window.Tracker = Tracker;