buriedPiont.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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. // 工具函数
  34. function getUrlParam(name) {
  35. var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
  36. var r = window.location.search.substr(1).match(reg);
  37. if (r != null) return decodeURIComponent(r[2]);
  38. return null;
  39. }
  40. function getCookie(name) {
  41. const matches = document.cookie.match(new RegExp(
  42. `(?:^|; )${name.replace(/([.$?*|{}()[]\\\/\+^])/g, '\\$1')}=([^;]*)`
  43. ));
  44. return matches ? decodeURIComponent(matches[1]) : undefined;
  45. }
  46. function setCookie(name, value, minutes) {
  47. let expires = "";
  48. if (minutes) {
  49. const date = new Date();
  50. date.setTime(date.getTime() + (minutes * 60 * 1000));
  51. expires = "; expires=" + date.toUTCString();
  52. }
  53. document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
  54. }
  55. function deleteCookie(name) {
  56. document.cookie = `${name}=; Max-Age=-99999999; path=/`;
  57. }
  58. // 获取浏览器信息
  59. function getBrowserInfo() {
  60. var ua = navigator.userAgent;
  61. var browser = 'Unknown';
  62. var isMobile = /Mobile|Android|iPhone|iPad|iPod|HarmonyOS|HMS/i.test(ua);
  63. var osType = 'Unknown';
  64. var osVersion = 'Unknown';
  65. // 系统类型和版本号判断
  66. if (/iPhone|iPad|iPod/i.test(ua)) {
  67. osType = 'iOS';
  68. var iosVersionMatch = ua.match(/OS (\d+)[_.](\d+)?([_.](\d+))?/i);
  69. if (iosVersionMatch) {
  70. osVersion = iosVersionMatch[1];
  71. if (iosVersionMatch[2]) osVersion += '.' + iosVersionMatch[2];
  72. if (iosVersionMatch[4]) osVersion += '.' + iosVersionMatch[4];
  73. }
  74. } else if (/Android/i.test(ua)) {
  75. osType = 'Android';
  76. var androidVersionMatch = ua.match(/Android ([\d.]+)/i);
  77. if (androidVersionMatch) {
  78. osVersion = androidVersionMatch[1];
  79. }
  80. } else if (/HarmonyOS|HMS/i.test(ua)) {
  81. osType = 'HarmonyOS';
  82. var harmonyVersionMatch = ua.match(/HarmonyOS[\s/]?([\d.]+)/i) || ua.match(/HMSCore[\s/]?([\d.]+)/i);
  83. if (harmonyVersionMatch) {
  84. osVersion = harmonyVersionMatch[1];
  85. }
  86. }
  87. // 浏览器类型判断
  88. if (/Edg/i.test(ua)) {
  89. browser = 'Edge';
  90. } else if (/HuaweiBrowser/i.test(ua) || /HMS/i.test(ua)) {
  91. browser = 'HuaweiBrowser';
  92. } else if (/Chrome/i.test(ua)) {
  93. browser = 'Chrome';
  94. } else if (/Firefox/i.test(ua)) {
  95. browser = 'Firefox';
  96. } else if (/Safari/i.test(ua) && !/Chrome/i.test(ua) && !/Edg/i.test(ua)) {
  97. browser = 'Safari';
  98. } else if (/MSIE|Trident/i.test(ua)) {
  99. browser = 'IE';
  100. }
  101. // 特殊浏览器环境检测
  102. if (/MicroMessenger/i.test(ua)) {
  103. browser = 'WeChat';
  104. } else if (/QQBrowser/i.test(ua)) {
  105. browser = 'QQBrowser';
  106. } else if (/UCBrowser/i.test(ua)) {
  107. browser = 'UCBrowser';
  108. } else if (/Telegram/i.test(ua)) {
  109. browser = 'Telegram';
  110. }
  111. return {
  112. isMobile: isMobile,
  113. browser: browser,
  114. userAgent: ua,
  115. osType: osType,
  116. osVersion: osVersion
  117. };
  118. }
  119. // 主类
  120. function UtmTracker() {}
  121. // 获取UTM参数
  122. UtmTracker.prototype.getParams = function() {
  123. return {
  124. utm_source: getUrlParam('utm_source') || '',
  125. utm_medium: getUrlParam('utm_medium') || '',
  126. utm_campaign: getUrlParam('utm_campaign') || '',
  127. utm_term: getUrlParam('utm_term') || '',
  128. utm_content: getUrlParam('utm_content') || '',
  129. referrer: document.referrer || '',
  130. browser: getBrowserInfo(),
  131. timestamp: new Date().toISOString(),
  132. url: window.location.href
  133. };
  134. };
  135. // 静态方法:快速获取(无需实例化)
  136. UtmTracker.get = function() {
  137. return new UtmTracker().getParams();
  138. };
  139. class Tracker {
  140. initCofig = {
  141. baseUrl: '',
  142. heartbeatInterval: 30000, // 心跳间隔,默认30秒
  143. visitCookieTimeout: 1, // 访问次数cookie有效期1分钟
  144. maxVisitCount: 5, // 最大访问次数5次
  145. blockCookieTimeout: 180, // 封锁cookie有效期180分钟
  146. myEventSend: () => {}, // 自定义事件上报函数
  147. beforeDestroy: () => {}, // 销毁前的回调函数
  148. crashTime: 3, // 闪退阈值(几秒内关闭)
  149. idleTimeout: 300000, // 默认 5 分钟无操作视为离开
  150. debounceTime: 300, // 防抖间隔,默认300毫秒
  151. }
  152. hasInitialized = false; // 是否已初始化
  153. initialize(options) {
  154. this.initCofig = {
  155. ...this.initCofig,
  156. ...options,
  157. }
  158. this.baseUrl = options.baseUrl || this.initCofig.baseUrl || 'https://your-default-tracking-api.com'; // 默认上报地址
  159. this.timer = null;
  160. this.userId = 'user'; // 用户ID
  161. this.startTime = 0;
  162. this.heartbeatInterval = options.heartbeatInterval || this.initCofig.heartbeatInterval; // 默认30秒发送一次心跳
  163. this.visitCookieTimeout = options.visitCookieTimeout || this.initCofig.visitCookieTimeout; // 默认访问次数cookie有效期1分钟
  164. this.maxVisitCount = options.maxVisitCount || this.initCofig.maxVisitCount; // 默认最大访问次数5次
  165. this.blockCookieTimeout = options.blockCookieTimeout || this.initCofig.blockCookieTimeout; // 默认封锁cookie有效期180分钟
  166. this.myEventSend = options.myEventSend || this.initCofig.myEventSend; // 是否开启自定义事件上报
  167. this.beforeDestroy = options.beforeDestroy || this.initCofig.beforeDestroy; // 销毁前的回调函数
  168. this.crashTime = options.crashTime || this.initCofig.crashTime; // 闪退阈值(几秒内关闭页面被认为是闪退)
  169. this.idleTimeout = options.idleTimeout || this.initCofig.idleTimeout; // 默认 5 分钟无操作视为离开
  170. this.sendDataDebounceTimers = {}; // 【2024-06-09】存储每种事件类型的定时器,用于事件级防抖
  171. this.debounceTime = options.debounceTime || this.debounceTime || 300; // 【2024-06-09】防抖间隔支持初始化参数传入
  172. window.JSBridge.getUserId((id) => {
  173. this.userId = id;
  174. });
  175. this.headers = {
  176. 'Content-Type': 'application/json',
  177. heartbeatSeconds: this.heartbeatInterval,
  178. userId: this.userId,
  179. };
  180. options.headers && Object.assign(this.headers, options.headers);
  181. }
  182. constructor(options = {}) {
  183. this.initialize(options);
  184. }
  185. init(options = {}) {
  186. if (this.hasInitialized) {
  187. console.warn('埋点已初始化,请勿重复调用');
  188. return
  189. }
  190. this.initialize(options);
  191. const visitCookieName = 'userVisitCount';
  192. const blockCookieName = 'userBlocked';
  193. const isBlocked = getCookie(blockCookieName);
  194. const doneFN = () => {
  195. this.startTime = Date.now();
  196. this.bindEvents();
  197. this.startHeartbeat();
  198. console.log(`埋点初始化成功,用户ID: ${this.userId}`);
  199. this.hasInitialized = true;
  200. }
  201. if (isBlocked) {
  202. // restoreBlock();
  203. return `用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`;
  204. }
  205. let visitCount = getCookie(visitCookieName);
  206. if (visitCount) {
  207. visitCount = parseInt(visitCount, 10);
  208. if (visitCount >= this.maxVisitCount) {
  209. setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟
  210. deleteCookie(visitCookieName);
  211. // restoreBlock();
  212. console.warn(`用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`);
  213. } else {
  214. visitCount += 1;
  215. setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟
  216. doneFN();
  217. }
  218. } else {
  219. setCookie(visitCookieName, '1', this.visitCookieTimeout); // 首次访问,设置访问次数为 1
  220. doneFN();
  221. }
  222. }
  223. bindEvents() {
  224. window.addEventListener('beforeunload', this.handleBeforeUnload);
  225. window.addEventListener('click', this.handleClickEvent);
  226. document.addEventListener('visibilitychange', this.handleVisibilityChange);
  227. window.addEventListener('mousemove', this.resetIdleTimer.bind(this));
  228. window.addEventListener('keydown', this.resetIdleTimer.bind(this));
  229. this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报
  230. }
  231. startHeartbeat() {
  232. this.timer = setInterval(() => {
  233. this.sendData({
  234. eventType: 'heartbeat',
  235. duration: Math.round((Date.now() - this.startTime) / 1000),
  236. });
  237. }, this.heartbeatInterval);
  238. }
  239. // 页面初始化调用,手动在不同框架的生命周期中调用
  240. markContentLoaded() {
  241. this.sendData({
  242. eventType: 'content_loaded',
  243. loadTime: Math.round((Date.now() - this.startTime) / 1000),
  244. });
  245. }
  246. // 用户无操作时发送不活跃事件
  247. resetIdleTimer() {
  248. if (this.idleTimer) {
  249. clearTimeout(this.idleTimer);
  250. }
  251. this.idleTimer = setTimeout(() => {
  252. this.sendData({
  253. eventType: 'user_inactive',
  254. duration: Math.round((Date.now() - this.startTime) / 1000),
  255. });
  256. }, this.idleTimeout);
  257. }
  258. handleBeforeUnload = (e) => {
  259. const duration = Math.round((Date.now() - this.startTime) / 1000);
  260. const data = {
  261. eventType: 'page_close',
  262. duration,
  263. exitType: duration > this.crashTime ? 'crash' : 'normal',
  264. };
  265. const payload = JSON.stringify(data);
  266. if (navigator.sendBeacon) {
  267. navigator.sendBeacon(this.baseUrl, payload);
  268. } else {
  269. this.sendData(payload);
  270. }
  271. };
  272. handleClickEvent = (event) => {
  273. const target = event.target;
  274. if (target.matches('[data-track]')) {
  275. const trackInfo = {
  276. eventType: target.getAttribute('event-type') || 'button_click',
  277. elementId: target.id || '无ID',
  278. elementClass: target.className || '无class',
  279. text: target.innerText || target.textContent || '',
  280. timestamp: new Date().toISOString(),
  281. };
  282. this.sendData(trackInfo);
  283. }
  284. };
  285. handleVisibilityChange = () => {
  286. if (document.hidden) {
  287. // 用户切换到其他 tab
  288. this.sendData({ eventType: 'tab_inactive' });
  289. } else {
  290. // 用户回到当前 tab
  291. this.sendData({ eventType: 'tab_active' });
  292. }
  293. };
  294. queueIdleTask(task, timeout = 1000) {
  295. if (typeof requestIdleCallback === 'function') {
  296. requestIdleCallback(() => {
  297. task();
  298. }, { timeout });
  299. } else {
  300. // 兜底方案:使用 setTimeout 模拟
  301. setTimeout(task, 0);
  302. }
  303. }
  304. sendData(data, headers = {}) {
  305. const eventType = data && data.eventType;
  306. // 【2024-06-09】只对有 eventType 且不是系统事件的做防抖,系统事件立即上报
  307. const systemEvents = [
  308. 'heartbeat', 'tab_active', 'tab_inactive', 'page_close',
  309. 'content_loaded', 'user_inactive', 'tracker_destroyed'
  310. ];
  311. if (eventType && !systemEvents.includes(eventType)) {
  312. // 【2024-06-09】每种事件类型单独防抖,互不影响
  313. if (this.sendDataDebounceTimers[eventType]) {
  314. clearTimeout(this.sendDataDebounceTimers[eventType]);
  315. }
  316. this.sendDataDebounceTimers[eventType] = setTimeout(() => {
  317. this._sendDataNow(data, headers);
  318. this.sendDataDebounceTimers[eventType] = null;
  319. }, this.debounceTime);
  320. } else {
  321. this._sendDataNow(data, headers);
  322. }
  323. }
  324. _sendDataNow(data, headers = {}) {
  325. this.queueIdleTask(() => {
  326. const UTMP = UtmTracker.get();
  327. const params = {
  328. ...data,
  329. ...UTMP,
  330. ...UTMP.browser,
  331. };
  332. console.log('上报埋点数据:', params);
  333. fetch(`${this.baseUrl}`, {
  334. method: 'POST',
  335. headers: this.headers,
  336. body: JSON.stringify(params)
  337. }).catch(err => {
  338. console.error('埋点失败:', err);
  339. });
  340. });
  341. }
  342. destroy() {
  343. window.removeEventListener('beforeunload', this.handleBeforeUnload);
  344. window.removeEventListener('click', this.handleClickEvent);
  345. document.removeEventListener('visibilitychange', this.handleVisibilityChange);
  346. window.removeEventListener('mousemove', this.resetIdleTimer);
  347. window.removeEventListener('keydown', this.resetIdleTimer);
  348. this.beforeDestroy();
  349. if (this.timer) {
  350. clearInterval(this.timer);
  351. }
  352. this.sendData({
  353. eventType: 'tracker_destroyed',
  354. duration: Math.round((Date.now() - this.startTime) / 1000),
  355. });
  356. this.hasInitialized = false;
  357. return '销毁埋点成功'
  358. }
  359. }
  360. // 使用示例:
  361. // const tracker = new Tracker({ baseUrl: 'https://your-tracking-api.com' });
  362. // tracker.init();
  363. // 暴露给全局或者作为模块导出
  364. window.Tracker = Tracker;