Просмотр исходного кода

Merge branch 'diy' of https://s-20coaj910c.zht2.com/luoy/buried-piont

# Conflicts:
#	main.html
#	vue-test/src/main.js
luoy 3 недель назад
Родитель
Сommit
1e7215a702

+ 3 - 1
README.md

@@ -4,4 +4,6 @@
   post-key值根据登录账号获取 phc_5YuF937Fs3N0djI4XFThsGAZfgaciU9pjKw6T3SQjvV
   post-host为默认发送到官网后台 https://us.posthog.com/
 
-#### 
+#### countly: https://support.countly.com/hc/en-us/articles/360037441932-Web-analytics-JavaScript
+
+#### 以上两种方式都需要注册账号获得服务器后台,替换key值后在官网后台查看埋点数据,付费后支持自定义部署

+ 365 - 0
buriedPiont.js

@@ -0,0 +1,365 @@
+
+// 通用 JSBridge
+window.JSBridge = {
+  getOS: function () {
+    const ua = navigator.userAgent || navigator.vendor || window.opera;
+    if (/android/i.test(ua)) return 'Android';
+    if (/iPad|iPhone|iPod/.test(ua) && !window.MSStream) return 'iOS';
+    return 'Web';
+  },
+  getUserId: function (callback) {
+    // Android WebView
+    if (window.Android && typeof window.Android.getUserId === 'function') {
+      callback(window.Android.getUserId());
+      return ;
+    }
+    // iOS WebView
+    if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getUserId) {
+      window.webkit.messageHandlers.getUserId.postMessage(null);
+      window.onUserIdReceived = callback; // iOS原生需回调window.onUserIdReceived(userId)
+      return;
+    }
+    // Web环境(用 FingerprintJS 生成指纹)
+    if (window.FingerprintJS) {
+      FingerprintJS.load().then(fp => {
+        fp.get().then(result => {
+          callback(result.visitorId);
+        });
+      });
+    } else {
+      callback('unsupported');
+    }
+  }
+};
+
+// 工具函数
+function getUrlParam(name) {
+    var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
+    var r = window.location.search.substr(1).match(reg);
+    if (r != null) return decodeURIComponent(r[2]);
+    return null;
+}
+function getCookie(name) {
+  const matches = document.cookie.match(new RegExp(
+    `(?:^|; )${name.replace(/([.$?*|{}()[]\\\/\+^])/g, '\\$1')}=([^;]*)`
+  ));
+  return matches ? decodeURIComponent(matches[1]) : undefined;
+}
+function setCookie(name, value, minutes) {
+  let expires = "";
+  if (minutes) {
+    const date = new Date();
+    date.setTime(date.getTime() + (minutes * 60 * 1000));
+    expires = "; expires=" + date.toUTCString();
+  }
+  document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
+}  
+function deleteCookie(name) {
+  document.cookie = `${name}=; Max-Age=-99999999; path=/`;
+}
+// 获取浏览器信息
+function getBrowserInfo() {
+    var ua = navigator.userAgent;
+    var browser = 'Unknown';
+    var isMobile = /Mobile|Android|iPhone|iPad|iPod|HarmonyOS|HMS/i.test(ua);
+    var osType = 'Unknown';
+    var osVersion = 'Unknown';
+
+    // 系统类型和版本号判断
+    if (/iPhone|iPad|iPod/i.test(ua)) {
+        osType = 'iOS';
+        var iosVersionMatch = ua.match(/OS (\d+)[_.](\d+)?([_.](\d+))?/i);
+        if (iosVersionMatch) {
+            osVersion = iosVersionMatch[1];
+            if (iosVersionMatch[2]) osVersion += '.' + iosVersionMatch[2];
+            if (iosVersionMatch[4]) osVersion += '.' + iosVersionMatch[4];
+        }
+    } else if (/Android/i.test(ua)) {
+        osType = 'Android';
+        var androidVersionMatch = ua.match(/Android ([\d.]+)/i);
+        if (androidVersionMatch) {
+            osVersion = androidVersionMatch[1];
+        }
+    } else if (/HarmonyOS|HMS/i.test(ua)) {
+        osType = 'HarmonyOS';
+        var harmonyVersionMatch = ua.match(/HarmonyOS[\s/]?([\d.]+)/i) || ua.match(/HMSCore[\s/]?([\d.]+)/i);
+        if (harmonyVersionMatch) {
+            osVersion = harmonyVersionMatch[1];
+        }
+    }
+
+    // 浏览器类型判断
+    if (/Edg/i.test(ua)) {
+        browser = 'Edge';
+    } else if (/HuaweiBrowser/i.test(ua) || /HMS/i.test(ua)) {
+        browser = 'HuaweiBrowser';
+    } else if (/Chrome/i.test(ua)) {
+        browser = 'Chrome';
+    } else if (/Firefox/i.test(ua)) {
+        browser = 'Firefox';
+    } else if (/Safari/i.test(ua) && !/Chrome/i.test(ua) && !/Edg/i.test(ua)) {
+        browser = 'Safari';
+    } else if (/MSIE|Trident/i.test(ua)) {
+        browser = 'IE';
+    }
+
+    // 特殊浏览器环境检测
+    if (/MicroMessenger/i.test(ua)) {
+        browser = 'WeChat';
+    } else if (/QQBrowser/i.test(ua)) {
+        browser = 'QQBrowser';
+    } else if (/UCBrowser/i.test(ua)) {
+        browser = 'UCBrowser';
+    } else if (/Telegram/i.test(ua)) {
+        browser = 'Telegram';
+    }
+
+    return {
+        isMobile: isMobile,
+        browser: browser,
+        userAgent: ua,
+        osType: osType,
+        osVersion: osVersion
+    };
+}
+
+// 主类
+function UtmTracker() {}
+
+// 获取UTM参数
+UtmTracker.prototype.getParams = function() {
+    return {
+        utm_source: getUrlParam('utm_source') || '',
+        utm_medium: getUrlParam('utm_medium') || '',
+        utm_campaign: getUrlParam('utm_campaign') || '',
+        utm_term: getUrlParam('utm_term') || '',
+        utm_content: getUrlParam('utm_content') || '',
+        referrer: document.referrer || '',
+        browser: getBrowserInfo(),
+        timestamp: new Date().toISOString(),
+        url: window.location.href
+    };
+};
+
+// 静态方法:快速获取(无需实例化)
+UtmTracker.get = function() {
+    return new UtmTracker().getParams();
+};
+
+class Tracker {
+  initCofig = {
+    baseUrl: '',
+    heartbeatInterval: 30000, // 心跳间隔,默认30秒
+    visitCookieTimeout: 1, // 访问次数cookie有效期1分钟
+    maxVisitCount: 5, // 最大访问次数5次
+    blockCookieTimeout: 180, // 封锁cookie有效期180分钟
+    myEventSend: () => {}, // 自定义事件上报函数
+    beforeDestroy: () => {}, // 销毁前的回调函数
+    crashTime: 3, // 闪退阈值(几秒内关闭)
+    idleTimeout: 300000, // 默认 5 分钟无操作视为离开
+    
+  }
+  hasInitialized = false; // 是否已初始化
+  initialize(options) {
+    this.initCofig = {
+      ...this.initCofig,
+      ...options,
+    }
+    this.baseUrl = options.baseUrl || this.initCofig.baseUrl || 'https://your-default-tracking-api.com'; // 默认上报地址
+    this.timer = null;
+    this.userId = 'user'; // 用户ID
+    this.startTime = 0;
+    this.heartbeatInterval = options.heartbeatInterval || this.initCofig.heartbeatInterval; // 默认30秒发送一次心跳
+    this.visitCookieTimeout = options.visitCookieTimeout || this.initCofig.visitCookieTimeout; // 默认访问次数cookie有效期1分钟
+    this.maxVisitCount = options.maxVisitCount || this.initCofig.maxVisitCount; // 默认最大访问次数5次
+    this.blockCookieTimeout = options.blockCookieTimeout || this.initCofig.blockCookieTimeout; // 默认封锁cookie有效期180分钟
+    this.myEventSend = options.myEventSend || this.initCofig.myEventSend; // 是否开启自定义事件上报
+    this.beforeDestroy = options.beforeDestroy || this.initCofig.beforeDestroy; // 销毁前的回调函数
+    this.crashTime = options.crashTime || this.initCofig.crashTime; // 闪退阈值(几秒内关闭页面被认为是闪退)
+    this.idleTimeout = options.idleTimeout || this.initCofig.idleTimeout; // 默认 5 分钟无操作视为离开
+
+    window.JSBridge.getUserId((id) => {
+      this.userId = id;
+    });
+
+    this.headers = {
+      'Content-Type': 'application/json',
+      heartbeatSeconds: this.heartbeatInterval,
+      userId: this.userId,
+    };
+    options.headers && Object.assign(this.headers, options.headers);
+  }
+  constructor(options = {}) {
+    this.initialize(options);
+  }
+
+  init(options = {}) {
+    if (this.hasInitialized) {
+      console.warn('埋点已初始化,请勿重复调用');
+      return
+    }
+    this.initialize(options);
+    const visitCookieName = 'userVisitCount';
+    const blockCookieName = 'userBlocked';
+    const isBlocked = getCookie(blockCookieName);
+    const doneFN = () => {
+      this.startTime = Date.now();
+      this.bindEvents();
+      this.startHeartbeat();
+      console.log(`埋点初始化成功,用户ID: ${this.userId}`);
+      this.hasInitialized = true;
+    }
+    if (isBlocked) {
+      // restoreBlock();
+      return `用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`;
+    }
+
+    let visitCount = getCookie(visitCookieName);
+    if (visitCount) {
+      visitCount = parseInt(visitCount, 10);
+      if (visitCount >= this.maxVisitCount) {
+        setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟
+        deleteCookie(visitCookieName);
+        // restoreBlock();
+        console.warn(`用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`);
+      } else {
+        visitCount += 1;
+        setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟
+        doneFN();
+      }
+    } else {
+      setCookie(visitCookieName, '1', this.visitCookieTimeout); // 首次访问,设置访问次数为 1
+      doneFN();
+    }
+  }
+
+  bindEvents() {
+    window.addEventListener('beforeunload', this.handleBeforeUnload);
+    window.addEventListener('click', this.handleClickEvent);
+    document.addEventListener('visibilitychange', this.handleVisibilityChange);
+    window.addEventListener('mousemove', this.resetIdleTimer.bind(this));
+    window.addEventListener('keydown', this.resetIdleTimer.bind(this));
+    this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报
+  }
+
+  startHeartbeat() {
+    this.timer = setInterval(() => {
+      this.sendData({
+        eventType: 'heartbeat',
+        duration: Math.round((Date.now() - this.startTime) / 1000),
+      });
+    }, this.heartbeatInterval);
+  }
+
+  // 页面初始化调用,手动在不同框架的生命周期中调用
+  markContentLoaded() {
+    this.sendData({
+      eventType: 'content_loaded',
+      loadTime: Math.round((Date.now() - this.startTime) / 1000),
+    });
+  }
+
+  // 用户无操作时发送不活跃事件
+  resetIdleTimer() {
+    if (this.idleTimer) {
+      clearTimeout(this.idleTimer);
+    }
+    this.idleTimer = setTimeout(() => {
+      this.sendData({
+        eventType: 'user_inactive',
+        duration: Math.round((Date.now() - this.startTime) / 1000),
+      });
+    }, this.idleTimeout);
+  }
+
+  handleBeforeUnload =  (e) => {
+    const duration = Math.round((Date.now() - this.startTime) / 1000);
+    const data = {
+      eventType: 'page_close',
+      duration,
+      exitType: duration > this.crashTime ? 'crash' : 'normal',
+    };
+    const payload = JSON.stringify(data);
+    if (navigator.sendBeacon) {
+      navigator.sendBeacon(this.baseUrl, payload);
+    } else {
+      this.sendData(payload);
+    }
+  };
+
+  handleClickEvent = (event) => {
+    const target = event.target;
+    if (target.matches('[data-track]')) {
+      const trackInfo = {
+        eventType: 'button_click',
+        elementId: target.id || '无ID',
+        elementClass: target.className || '无class',
+        text: target.innerText || target.textContent || '',
+        timestamp: new Date().toISOString(),
+      };
+      this.sendData(trackInfo);
+    }
+  };
+
+  handleVisibilityChange = () => {
+    if (document.hidden) {
+      // 用户切换到其他 tab
+      this.sendData({ eventType: 'tab_inactive' });
+    } else {
+      // 用户回到当前 tab
+      this.sendData({ eventType: 'tab_active' });
+    }
+  };
+  queueIdleTask(task, timeout = 1000) {
+    if (typeof requestIdleCallback === 'function') {
+      requestIdleCallback(() => {
+        task();
+      }, { timeout });
+    } else {
+      // 兜底方案:使用 setTimeout 模拟
+      setTimeout(task, 0);
+    }
+  }
+  sendData(data, headers = {}) {
+    this.queueIdleTask(() => {
+      const params =  {
+        ...data,
+        ...UtmTracker.get()
+      };
+      params.browser = params.browser.browser || 'Unknown';
+      console.log('上报埋点数据:', params);
+      fetch(`${this.baseUrl}`, {
+        method: 'POST',
+        headers: this.headers,
+        body: JSON.stringify(params)
+      }).catch(err => {
+        console.error('埋点失败:', err);
+      });
+    });
+  }
+
+  destroy() {
+    window.removeEventListener('beforeunload', this.handleBeforeUnload);
+    window.removeEventListener('click', this.handleClickEvent);
+    document.removeEventListener('visibilitychange', this.handleVisibilityChange);
+    window.removeEventListener('mousemove', this.resetIdleTimer);
+    window.removeEventListener('keydown', this.resetIdleTimer);
+    this.beforeDestroy();
+    if (this.timer) {
+      clearInterval(this.timer);
+    }
+    this.sendData({
+      eventType: 'tracker_destroyed',
+      duration: Math.round((Date.now() - this.startTime) / 1000),
+    });
+    this.hasInitialized = false;
+    return '销毁埋点成功'
+  }
+}
+
+// 使用示例:
+// const tracker = new Tracker({ baseUrl: 'https://your-tracking-api.com' });
+// tracker.init();
+
+// 暴露给全局或者作为模块导出
+window.Tracker = Tracker;

+ 16 - 2
main.html

@@ -7,9 +7,23 @@
 
 <body>
   这是一个空页面
-  <button id="btn">点击我</button>
+  <button id="btn" data-track>点击我</button>
+  <div>操作系统: <span id="os"></span></div>
+  <div>用户唯一标识: <span id="userId"></span></div>
 </body>
-
+<script src="buriedPiont.js"></script>
 <script>
+  // 展示操作系统和用户唯一标识
+  document.addEventListener('DOMContentLoaded', function () {
+    document.getElementById('os').innerText = JSBridge.getOS();
+    JSBridge.getUserId(function (userId) {
+      document.getElementById('userId').innerText = userId;
+    });
+  });
+  const tracker = new Tracker({ 
+    baseUrl: 'http://192.168.3.9:3000/api/log',
+    heartbeatInterval: 5000,
+  });
+  tracker.init();
 </script>
 </html>

+ 7 - 47
react-test/package-lock.json

@@ -12,7 +12,7 @@
         "@testing-library/jest-dom": "^6.6.3",
         "@testing-library/react": "^16.3.0",
         "@testing-library/user-event": "^13.5.0",
-        "posthog-js": "^1.256.1",
+        "countly-sdk-web": "^25.4.1",
         "react": "^19.1.0",
         "react-dom": "^19.1.0",
         "react-scripts": "5.0.1",
@@ -5956,6 +5956,12 @@
         "node": ">=10"
       }
     },
+    "node_modules/countly-sdk-web": {
+      "version": "25.4.1",
+      "resolved": "https://registry.npmjs.org/countly-sdk-web/-/countly-sdk-web-25.4.1.tgz",
+      "integrity": "sha512-7xU6pZ42mp/kgu73cbUJMq3Kvk5RIofOIzOcrbSsxX/RnTcFjvYAly0H9tGi4FTcnPURXsfmuOcSpvK727h8Uw==",
+      "license": "MIT"
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -8000,12 +8006,6 @@
         "bser": "2.1.1"
       }
     },
-    "node_modules/fflate": {
-      "version": "0.4.8",
-      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
-      "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
-      "license": "MIT"
-    },
     "node_modules/file-entry-cache": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -13515,46 +13515,6 @@
       "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
       "license": "MIT"
     },
-    "node_modules/posthog-js": {
-      "version": "1.256.1",
-      "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.256.1.tgz",
-      "integrity": "sha512-cBLc3W1BHHzxYlJc6kIDbbPVUFTRbsObG0Cn1CnEG7lcyWKbalmW1XvUw7+lu/yyHYfCf7hwTTBJF7GgGFFMmw==",
-      "license": "SEE LICENSE IN LICENSE",
-      "dependencies": {
-        "core-js": "^3.38.1",
-        "fflate": "^0.4.8",
-        "preact": "^10.19.3",
-        "web-vitals": "^4.2.4"
-      },
-      "peerDependencies": {
-        "@rrweb/types": "2.0.0-alpha.17",
-        "rrweb-snapshot": "2.0.0-alpha.17"
-      },
-      "peerDependenciesMeta": {
-        "@rrweb/types": {
-          "optional": true
-        },
-        "rrweb-snapshot": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/posthog-js/node_modules/web-vitals": {
-      "version": "4.2.4",
-      "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
-      "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
-      "license": "Apache-2.0"
-    },
-    "node_modules/preact": {
-      "version": "10.26.9",
-      "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz",
-      "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==",
-      "license": "MIT",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/preact"
-      }
-    },
     "node_modules/prelude-ls": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

+ 0 - 1
react-test/package.json

@@ -7,7 +7,6 @@
     "@testing-library/jest-dom": "^6.6.3",
     "@testing-library/react": "^16.3.0",
     "@testing-library/user-event": "^13.5.0",
-    "posthog-js": "^1.256.1",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "react-scripts": "5.0.1",

+ 1 - 7
react-test/src/index.js

@@ -3,17 +3,11 @@ import ReactDOM from 'react-dom/client';
 import './index.css';
 import App from './App';
 import reportWebVitals from './reportWebVitals';
-import { PostHogProvider } from 'posthog-js/react'
 
 const root = ReactDOM.createRoot(document.getElementById('root'));
-const options = {
-  api_host: process.env.REACT_APP_POSTHOG_HOST,
-}
 root.render(
   <React.StrictMode>
-    <PostHogProvider apiKey={process.env.REACT_APP_POSTHOG_KEY} options={options}>
-      <App />
-    </PostHogProvider>
+    <App />
   </React.StrictMode>
 );
 

Разница между файлами не показана из-за своего большого размера
+ 9 - 769
vue-test/package-lock.json


+ 2 - 6
vue-test/package.json

@@ -9,17 +9,13 @@
   },
   "dependencies": {
     "core-js": "^3.8.3",
-    "posthog-js": "^1.256.1",
+    "utm-params-extractor-test": "^1.0.9",
     "vue": "^3.2.13"
   },
   "devDependencies": {
     "@babel/core": "^7.12.16",
-    "@babel/eslint-parser": "^7.12.16",
     "@vue/cli-plugin-babel": "~5.0.0",
-    "@vue/cli-plugin-eslint": "~5.0.0",
-    "@vue/cli-service": "~5.0.0",
-    "eslint": "^7.32.0",
-    "eslint-plugin-vue": "^8.0.3"
+    "@vue/cli-service": "~5.0.0"
   },
   "eslintConfig": {
     "root": true,

+ 14 - 0
vue-test/public/index.html

@@ -14,4 +14,18 @@
     <div id="app"></div>
     <!-- built files will be auto injected -->
   </body>
+  <!-- <script src="https://cdn.bootcdn.net/ajax/libs/vConsole/3.3.4/vconsole.min.js"></script> -->
+  <!-- <script src="https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@3/dist/fp.min.js"></script> -->
+  <!-- <script>
+    (async () => {
+
+      const v = new window.VConsole()
+      // console.info(window.screen)
+      // const fp = await FingerprintJS.load();
+      // const result = await fp.get();
+      // const visitorId = result.visitorId;
+      // console.log('------------(Visitor ID):', visitorId);
+
+    })();
+</script> -->
 </html>

+ 366 - 0
vue-test/src/buriedPiont.js

@@ -0,0 +1,366 @@
+
+// 通用 JSBridge
+window.JSBridge = {
+  getOS: function () {
+    const ua = navigator.userAgent || navigator.vendor || window.opera;
+    if (/android/i.test(ua)) return 'Android';
+    if (/iPad|iPhone|iPod/.test(ua) && !window.MSStream) return 'iOS';
+    return 'Web';
+  },
+  getUserId: function (callback) {
+    // Android WebView
+    if (window.Android && typeof window.Android.getUserId === 'function') {
+      callback(window.Android.getUserId());
+      return ;
+    }
+    // iOS WebView
+    if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getUserId) {
+      window.webkit.messageHandlers.getUserId.postMessage(null);
+      window.onUserIdReceived = callback; // iOS原生需回调window.onUserIdReceived(userId)
+      return;
+    }
+    // Web环境(用 FingerprintJS 生成指纹)
+    if (window.FingerprintJS) {
+      FingerprintJS.load().then(fp => {
+        fp.get().then(result => {
+          callback(result.visitorId);
+        });
+      });
+    } else {
+      callback('unsupported');
+    }
+  }
+};
+
+// 工具函数
+function getUrlParam(name) {
+    var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
+    var r = window.location.search.substr(1).match(reg);
+    if (r != null) return decodeURIComponent(r[2]);
+    return null;
+}
+function getCookie(name) {
+  const matches = document.cookie.match(new RegExp(
+    `(?:^|; )${name.replace(/([.$?*|{}()[]\\\/\+^])/g, '\\$1')}=([^;]*)`
+  ));
+  return matches ? decodeURIComponent(matches[1]) : undefined;
+}
+function setCookie(name, value, minutes) {
+  let expires = "";
+  if (minutes) {
+    const date = new Date();
+    date.setTime(date.getTime() + (minutes * 60 * 1000));
+    expires = "; expires=" + date.toUTCString();
+  }
+  document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
+}  
+function deleteCookie(name) {
+  document.cookie = `${name}=; Max-Age=-99999999; path=/`;
+}
+// 获取浏览器信息
+function getBrowserInfo() {
+    var ua = navigator.userAgent;
+    var browser = 'Unknown';
+    var isMobile = /Mobile|Android|iPhone|iPad|iPod|HarmonyOS|HMS/i.test(ua);
+    var osType = 'Unknown';
+    var osVersion = 'Unknown';
+
+    // 系统类型和版本号判断
+    if (/iPhone|iPad|iPod/i.test(ua)) {
+        osType = 'iOS';
+        var iosVersionMatch = ua.match(/OS (\d+)[_.](\d+)?([_.](\d+))?/i);
+        if (iosVersionMatch) {
+            osVersion = iosVersionMatch[1];
+            if (iosVersionMatch[2]) osVersion += '.' + iosVersionMatch[2];
+            if (iosVersionMatch[4]) osVersion += '.' + iosVersionMatch[4];
+        }
+    } else if (/Android/i.test(ua)) {
+        osType = 'Android';
+        var androidVersionMatch = ua.match(/Android ([\d.]+)/i);
+        if (androidVersionMatch) {
+            osVersion = androidVersionMatch[1];
+        }
+    } else if (/HarmonyOS|HMS/i.test(ua)) {
+        osType = 'HarmonyOS';
+        var harmonyVersionMatch = ua.match(/HarmonyOS[\s/]?([\d.]+)/i) || ua.match(/HMSCore[\s/]?([\d.]+)/i);
+        if (harmonyVersionMatch) {
+            osVersion = harmonyVersionMatch[1];
+        }
+    }
+
+    // 浏览器类型判断
+    if (/Edg/i.test(ua)) {
+        browser = 'Edge';
+    } else if (/HuaweiBrowser/i.test(ua) || /HMS/i.test(ua)) {
+        browser = 'HuaweiBrowser';
+    } else if (/Chrome/i.test(ua)) {
+        browser = 'Chrome';
+    } else if (/Firefox/i.test(ua)) {
+        browser = 'Firefox';
+    } else if (/Safari/i.test(ua) && !/Chrome/i.test(ua) && !/Edg/i.test(ua)) {
+        browser = 'Safari';
+    } else if (/MSIE|Trident/i.test(ua)) {
+        browser = 'IE';
+    }
+
+    // 特殊浏览器环境检测
+    if (/MicroMessenger/i.test(ua)) {
+        browser = 'WeChat';
+    } else if (/QQBrowser/i.test(ua)) {
+        browser = 'QQBrowser';
+    } else if (/UCBrowser/i.test(ua)) {
+        browser = 'UCBrowser';
+    } else if (/Telegram/i.test(ua)) {
+        browser = 'Telegram';
+    }
+
+    return {
+        isMobile: isMobile,
+        browser: browser,
+        userAgent: ua,
+        osType: osType,
+        osVersion: osVersion
+    };
+}
+
+// 主类
+function UtmTracker() {}
+
+// 获取UTM参数
+UtmTracker.prototype.getParams = function() {
+    return {
+        utm_source: getUrlParam('utm_source') || '',
+        utm_medium: getUrlParam('utm_medium') || '',
+        utm_campaign: getUrlParam('utm_campaign') || '',
+        utm_term: getUrlParam('utm_term') || '',
+        utm_content: getUrlParam('utm_content') || '',
+        referrer: document.referrer || '',
+        browser: getBrowserInfo(),
+        timestamp: new Date().toISOString(),
+        url: window.location.href
+    };
+};
+
+// 静态方法:快速获取(无需实例化)
+UtmTracker.get = function() {
+    return new UtmTracker().getParams();
+};
+
+class Tracker {
+  initCofig = {
+    baseUrl: '',
+    heartbeatInterval: 30000, // 心跳间隔,默认30秒
+    visitCookieTimeout: 1, // 访问次数cookie有效期1分钟
+    maxVisitCount: 5, // 最大访问次数5次
+    blockCookieTimeout: 180, // 封锁cookie有效期180分钟
+    myEventSend: () => {}, // 自定义事件上报函数
+    beforeDestroy: () => {}, // 销毁前的回调函数
+    crashTime: 3, // 闪退阈值(几秒内关闭)
+    idleTimeout: 300000, // 默认 5 分钟无操作视为离开
+    
+  }
+  hasInitialized = false; // 是否已初始化
+  initialize(options) {
+    this.initCofig = {
+      ...this.initCofig,
+      ...options,
+    }
+    this.baseUrl = options.baseUrl || this.initCofig.baseUrl || 'https://your-default-tracking-api.com'; // 默认上报地址
+    this.timer = null;
+    this.userId = 'user'; // 用户ID
+    this.startTime = 0;
+    this.heartbeatInterval = options.heartbeatInterval || this.initCofig.heartbeatInterval; // 默认30秒发送一次心跳
+    this.visitCookieTimeout = options.visitCookieTimeout || this.initCofig.visitCookieTimeout; // 默认访问次数cookie有效期1分钟
+    this.maxVisitCount = options.maxVisitCount || this.initCofig.maxVisitCount; // 默认最大访问次数5次
+    this.blockCookieTimeout = options.blockCookieTimeout || this.initCofig.blockCookieTimeout; // 默认封锁cookie有效期180分钟
+    this.myEventSend = options.myEventSend || this.initCofig.myEventSend; // 是否开启自定义事件上报
+    this.beforeDestroy = options.beforeDestroy || this.initCofig.beforeDestroy; // 销毁前的回调函数
+    this.crashTime = options.crashTime || this.initCofig.crashTime; // 闪退阈值(几秒内关闭页面被认为是闪退)
+    this.idleTimeout = options.idleTimeout || this.initCofig.idleTimeout; // 默认 5 分钟无操作视为离开
+
+    window.JSBridge.getUserId((id) => {
+      this.userId = id;
+    });
+
+    this.headers = {
+      'Content-Type': 'application/json',
+      heartbeatSeconds: this.heartbeatInterval,
+      userId: this.userId,
+    };
+    options.headers && Object.assign(this.headers, options.headers);
+  }
+  constructor(options = {}) {
+    this.initialize(options);
+  }
+
+  init(options = {}) {
+    if (this.hasInitialized) {
+      console.warn('埋点已初始化,请勿重复调用');
+      return
+    }
+    this.initialize(options);
+    const visitCookieName = 'userVisitCount';
+    const blockCookieName = 'userBlocked';
+    const isBlocked = getCookie(blockCookieName);
+    const doneFN = () => {
+      this.startTime = Date.now();
+      this.bindEvents();
+      this.startHeartbeat();
+      console.log(`埋点初始化成功,用户ID: ${this.userId}`);
+      this.hasInitialized = true;
+    }
+    if (isBlocked) {
+      // restoreBlock();
+      return `用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`;
+    }
+
+    let visitCount = getCookie(visitCookieName);
+    if (visitCount) {
+      visitCount = parseInt(visitCount, 10);
+      if (visitCount >= this.maxVisitCount) {
+        setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟
+        deleteCookie(visitCookieName);
+        // restoreBlock();
+        console.warn(`用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`);
+      } else {
+        visitCount += 1;
+        setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟
+        doneFN();
+      }
+    } else {
+      setCookie(visitCookieName, '1', this.visitCookieTimeout); // 首次访问,设置访问次数为 1
+      doneFN();
+    }
+  }
+
+  bindEvents() {
+    window.addEventListener('beforeunload', this.handleBeforeUnload);
+    window.addEventListener('click', this.handleClickEvent);
+    document.addEventListener('visibilitychange', this.handleVisibilityChange);
+    window.addEventListener('mousemove', this.resetIdleTimer.bind(this));
+    window.addEventListener('keydown', this.resetIdleTimer.bind(this));
+    this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报
+  }
+
+  startHeartbeat() {
+    this.timer = setInterval(() => {
+      this.sendData({
+        eventType: 'heartbeat',
+        duration: Math.round((Date.now() - this.startTime) / 1000),
+      });
+    }, this.heartbeatInterval);
+  }
+
+  // 页面初始化调用,手动在不同框架的生命周期中调用
+  markContentLoaded() {
+    this.sendData({
+      eventType: 'content_loaded',
+      loadTime: Math.round((Date.now() - this.startTime) / 1000),
+    });
+  }
+
+  // 用户无操作时发送不活跃事件
+  resetIdleTimer() {
+    if (this.idleTimer) {
+      clearTimeout(this.idleTimer);
+    }
+    this.idleTimer = setTimeout(() => {
+      this.sendData({
+        eventType: 'user_inactive',
+        duration: Math.round((Date.now() - this.startTime) / 1000),
+      });
+    }, this.idleTimeout);
+  }
+
+  handleBeforeUnload =  (e) => {
+    const duration = Math.round((Date.now() - this.startTime) / 1000);
+    const data = {
+      eventType: 'page_close',
+      duration,
+      exitType: duration > this.crashTime ? 'crash' : 'normal',
+    };
+    const payload = JSON.stringify(data);
+    if (navigator.sendBeacon) {
+      navigator.sendBeacon(this.baseUrl, payload);
+    } else {
+      this.sendData(payload);
+    }
+  };
+
+  handleClickEvent = (event) => {
+    const target = event.target;
+    if (target.matches('[data-track]')) {
+      const trackInfo = {
+        eventType: 'button_click',
+        elementId: target.id || '无ID',
+        elementClass: target.className || '无class',
+        text: target.innerText || target.textContent || '',
+        timestamp: new Date().toISOString(),
+      };
+      this.sendData(trackInfo);
+    }
+  };
+
+  handleVisibilityChange = () => {
+    if (document.hidden) {
+      // 用户切换到其他 tab
+      this.sendData({ eventType: 'tab_inactive' });
+    } else {
+      // 用户回到当前 tab
+      this.sendData({ eventType: 'tab_active' });
+    }
+  };
+  queueIdleTask(task, timeout = 1000) {
+    if (typeof requestIdleCallback === 'function') {
+      requestIdleCallback(() => {
+        task();
+      }, { timeout });
+    } else {
+      // 兜底方案:使用 setTimeout 模拟
+      setTimeout(task, 0);
+    }
+  }
+  sendData(data, headers = {}) {
+    this.queueIdleTask(() => {
+      const UTMP = UtmTracker.get();
+      const params =  {
+        ...data,
+        ...UTMP,
+        ...UTMP.browser,
+      };
+      console.log('上报埋点数据:', params);
+      fetch(`${this.baseUrl}`, {
+        method: 'POST',
+        headers: this.headers,
+        body: JSON.stringify(params)
+      }).catch(err => {
+        console.error('埋点失败:', err);
+      });
+    });
+  }
+
+  destroy() {
+    window.removeEventListener('beforeunload', this.handleBeforeUnload);
+    window.removeEventListener('click', this.handleClickEvent);
+    document.removeEventListener('visibilitychange', this.handleVisibilityChange);
+    window.removeEventListener('mousemove', this.resetIdleTimer);
+    window.removeEventListener('keydown', this.resetIdleTimer);
+    this.beforeDestroy();
+    if (this.timer) {
+      clearInterval(this.timer);
+    }
+    this.sendData({
+      eventType: 'tracker_destroyed',
+      duration: Math.round((Date.now() - this.startTime) / 1000),
+    });
+    this.hasInitialized = false;
+    return '销毁埋点成功'
+  }
+}
+
+// 使用示例:
+// const tracker = new Tracker({ baseUrl: 'https://your-tracking-api.com' });
+// tracker.init();
+
+// 暴露给全局或者作为模块导出
+window.Tracker = Tracker;

+ 5 - 3
vue-test/src/components/HelloWorld.vue

@@ -1,8 +1,10 @@
 <template>
  <div>
   这是一个空页面
-  <button>
-   点击按钮
+  <button data-track="点了按钮">
+   点击按钮,发送数据
   </button>
  </div>
-</template>
+</template>
+<script setup>
+</script>

+ 8 - 1
vue-test/src/main.js

@@ -1,4 +1,11 @@
 import { createApp } from 'vue'
 import App from './App.vue'
-
+import './buriedPiont.js'
+// 方便测试修改配置项所有挂载到window对象上
+window.tracker = new Tracker({ 
+  baseUrl: 'http://192.168.3.16:3888/api/log',
+  heartbeatInterval: 50000,
+});
+tracker.init();
 createApp(App).mount('#app')
+

Разница между файлами не показана из-за своего большого размера
+ 14 - 550
vue-test/yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов