buriedPiont.js 5.6 KB

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