(function() { 'use strict'; // PageViewAI 追踪器 var PageViewAI = { config: { siteId: null, endpoint: 'https://pageviewai.com/api/tracking/event', debug: false }, // 初始化 init: function(siteId, options) { console.log('PageViewAI init called with:', siteId, options); this.config.siteId = siteId; if (options) { Object.assign(this.config, options); } console.log('PageViewAI config after init:', this.config); if (this.config.debug) { console.log('PageViewAI initialized with site ID:', siteId); } }, // 发送事件 send: function(eventType, data) { console.log('PageViewAI send called with:', eventType, data); console.log('Current config:', this.config); if (!this.config.siteId) { console.warn('PageViewAI: Site ID not configured. Current siteId:', this.config.siteId); return; } console.log('PageViewAI: Sending event with site ID:', this.config.siteId); var eventData = { site_id: this.config.siteId, event_type: eventType, page_url: window.location.href, page_title: document.title, referrer: document.referrer || '', user_agent: navigator.userAgent, screen_resolution: screen.width + 'x' + screen.height, viewport_size: window.innerWidth + 'x' + window.innerHeight, language: navigator.language || navigator.userLanguage, timestamp: new Date().toISOString(), session_id: this.getSessionId(), visitor_id: this.getVisitorId() }; // 提取搜索关键词 var searchInfo = this.extractSearchKeywords(document.referrer); if (searchInfo) { eventData.search_engine = searchInfo.engine; eventData.search_keywords = searchInfo.keywords; eventData.traffic_source = searchInfo.isOrganic ? 'organic' : 'paid'; } // 合并自定义数据 if (data && typeof data === 'object') { Object.assign(eventData, data); } // 发送数据 this.sendData(eventData); }, // 获取会话ID getSessionId: function() { var sessionId = sessionStorage.getItem('pv_session_id'); if (!sessionId) { sessionId = this.generateId(); sessionStorage.setItem('pv_session_id', sessionId); } return sessionId; }, // 获取访客ID getVisitorId: function() { var visitorId = localStorage.getItem('pv_visitor_id'); if (!visitorId) { visitorId = this.generateId(); localStorage.setItem('pv_visitor_id', visitorId); } return visitorId; }, // 生成唯一ID generateId: function() { return 'pv_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now(); }, // 发送数据到服务器 sendData: function(data) { console.log('PageViewAI sendData called with:', data); console.log('Sending to endpoint:', this.config.endpoint); try { // 使用 sendBeacon 如果可用 if (navigator.sendBeacon) { var blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); console.log('Using sendBeacon to send data'); var result = navigator.sendBeacon(this.config.endpoint, blob); console.log('sendBeacon result:', result); } else { // 降级到 fetch fetch(this.config.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), keepalive: true }).catch(function(error) { if (PageViewAI.config.debug) { console.error('PageViewAI tracking error:', error); } }); } if (this.config.debug) { console.log('PageViewAI event sent:', data); } } catch (error) { if (this.config.debug) { console.error('PageViewAI error:', error); } } }, // 自动追踪页面浏览 trackPageview: function() { this.send('pageview'); }, // 追踪自定义事件 trackEvent: function(eventName, properties) { this.send('event', { event_name: eventName, properties: properties || {} }); }, // 追踪页面离开 trackPageLeave: function() { var startTime = this.pageStartTime || Date.now(); var duration = Math.round((Date.now() - startTime) / 1000); this.send('page_leave', { duration: duration }); }, // 发送心跳 sendHeartbeat: function() { this.send('heartbeat'); }, // 启动心跳 startHeartbeat: function() { var self = this; // 每30秒发送一次心跳 this.heartbeatInterval = setInterval(function() { self.sendHeartbeat(); }, 30000); if (this.config.debug) { console.log('PageViewAI: Heartbeat started'); } }, // 停止心跳 stopHeartbeat: function() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; if (this.config.debug) { console.log('PageViewAI: Heartbeat stopped'); } } }, // 收集性能指标 collectPerformanceMetrics: function() { var metrics = {}; try { // 检查 Performance API 支持 if (!window.performance || !window.performance.timing) { return null; } var timing = window.performance.timing; var navigation = window.performance.navigation; // 基础页面加载时间指标 if (timing.loadEventEnd && timing.navigationStart) { metrics.page_load_time = timing.loadEventEnd - timing.navigationStart; } if (timing.domContentLoadedEventEnd && timing.navigationStart) { metrics.dom_content_loaded = timing.domContentLoadedEventEnd - timing.navigationStart; } if (timing.domComplete && timing.navigationStart) { metrics.dom_complete = timing.domComplete - timing.navigationStart; } // 网络时间指标 if (timing.domainLookupEnd && timing.domainLookupStart) { metrics.dns_lookup_time = timing.domainLookupEnd - timing.domainLookupStart; } if (timing.connectEnd && timing.connectStart) { metrics.tcp_connect_time = timing.connectEnd - timing.connectStart; } if (timing.secureConnectionStart && timing.connectEnd) { metrics.ssl_negotiation_time = timing.connectEnd - timing.secureConnectionStart; } if (timing.responseStart && timing.requestStart) { metrics.request_time = timing.responseStart - timing.requestStart; } if (timing.responseEnd && timing.responseStart) { metrics.response_time = timing.responseEnd - timing.responseStart; } // 资源加载信息 if (window.performance.getEntriesByType) { var resources = window.performance.getEntriesByType('resource'); metrics.total_resources_count = resources.length; var totalSize = 0; resources.forEach(function(resource) { if (resource.transferSize) { totalSize += resource.transferSize; } }); metrics.total_resources_size = totalSize; } // 网络连接信息 if (navigator.connection) { var connection = navigator.connection; metrics.connection_type = connection.effectiveType || connection.type; metrics.connection_downlink = connection.downlink; metrics.connection_rtt = connection.rtt; } // 设备内存信息 if (navigator.deviceMemory) { metrics.device_memory = navigator.deviceMemory; } return metrics; } catch (e) { if (this.config.debug) { console.error('Error collecting performance metrics:', e); } return null; } }, // 收集 Core Web Vitals 指标 collectWebVitals: function(callback) { var self = this; var vitals = {}; try { // First Contentful Paint (FCP) if (window.PerformanceObserver) { var fcpObserver = new PerformanceObserver(function(list) { var entries = list.getEntries(); entries.forEach(function(entry) { if (entry.name === 'first-contentful-paint') { vitals.first_contentful_paint = Math.round(entry.startTime); } }); }); try { fcpObserver.observe({ entryTypes: ['paint'] }); } catch (e) { // 忽略不支持的浏览器 } // Largest Contentful Paint (LCP) var lcpObserver = new PerformanceObserver(function(list) { var entries = list.getEntries(); var lastEntry = entries[entries.length - 1]; if (lastEntry) { vitals.largest_contentful_paint = Math.round(lastEntry.startTime); } }); try { lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] }); } catch (e) { // 忽略不支持的浏览器 } // First Input Delay (FID) var fidObserver = new PerformanceObserver(function(list) { var entries = list.getEntries(); entries.forEach(function(entry) { vitals.first_input_delay = Math.round(entry.processingStart - entry.startTime); }); }); try { fidObserver.observe({ entryTypes: ['first-input'] }); } catch (e) { // 忽略不支持的浏览器 } // Cumulative Layout Shift (CLS) var clsValue = 0; var clsObserver = new PerformanceObserver(function(list) { var entries = list.getEntries(); entries.forEach(function(entry) { if (!entry.hadRecentInput) { clsValue += entry.value; vitals.cumulative_layout_shift = clsValue; } }); }); try { clsObserver.observe({ entryTypes: ['layout-shift'] }); } catch (e) { // 忽略不支持的浏览器 } } // 延迟收集指标,确保页面加载完成 setTimeout(function() { if (callback && typeof callback === 'function') { callback(vitals); } }, 2000); } catch (e) { if (self.config.debug) { console.error('Error collecting Web Vitals:', e); } if (callback && typeof callback === 'function') { callback({}); } } }, // 发送性能指标 sendPerformanceMetrics: function() { var self = this; // 等待页面加载完成 if (document.readyState !== 'complete') { window.addEventListener('load', function() { setTimeout(function() { self.sendPerformanceMetrics(); }, 1000); }); return; } var performanceMetrics = this.collectPerformanceMetrics(); if (!performanceMetrics) { return; } // 收集 Web Vitals this.collectWebVitals(function(webVitals) { // 合并所有性能指标 var allMetrics = Object.assign({}, performanceMetrics, webVitals); // 发送性能数据 self.send('performance', allMetrics); }); }, // 提取搜索关键词 extractSearchKeywords: function(referrer) { if (!referrer) return null; try { var url = new URL(referrer); var hostname = url.hostname.toLowerCase(); var searchParams = url.searchParams; // Google 搜索 if (hostname.includes('google.')) { var keywords = searchParams.get('q') || searchParams.get('query'); if (keywords) { return { engine: 'Google', keywords: decodeURIComponent(keywords), isOrganic: !searchParams.get('gclid') }; } // Google 加密搜索 if (url.pathname.includes('/search') || url.pathname.includes('/url')) { return { engine: 'Google', keywords: '(not provided)', isOrganic: !searchParams.get('gclid') }; } } // Bing 搜索 if (hostname.includes('bing.com')) { var keywords = searchParams.get('q') || searchParams.get('search'); if (keywords) { return { engine: 'Bing', keywords: decodeURIComponent(keywords), isOrganic: !searchParams.get('msclkid') }; } } // 百度搜索 if (hostname.includes('baidu.com')) { var keywords = searchParams.get('wd') || searchParams.get('word') || searchParams.get('query'); if (keywords) { return { engine: 'Baidu', keywords: decodeURIComponent(keywords), isOrganic: true }; } } // Yahoo 搜索 if (hostname.includes('yahoo.com')) { var keywords = searchParams.get('p') || searchParams.get('q'); if (keywords) { return { engine: 'Yahoo', keywords: decodeURIComponent(keywords), isOrganic: true }; } } // DuckDuckGo 搜索 if (hostname.includes('duckduckgo.com')) { var keywords = searchParams.get('q'); if (keywords) { return { engine: 'DuckDuckGo', keywords: decodeURIComponent(keywords), isOrganic: true }; } } // 搜狗搜索 if (hostname.includes('sogou.com')) { var keywords = searchParams.get('query') || searchParams.get('keyword'); if (keywords) { return { engine: 'Sogou', keywords: decodeURIComponent(keywords), isOrganic: true }; } } // 360搜索 if (hostname.includes('so.com') || hostname.includes('360.cn')) { var keywords = searchParams.get('q'); if (keywords) { return { engine: '360', keywords: decodeURIComponent(keywords), isOrganic: true }; } } } catch (e) { // 忽略无效 URL if (this.config.debug) { console.error('Error parsing referrer:', e); } } return null; } }; // 记录页面开始时间 PageViewAI.pageStartTime = Date.now(); // 监听页面卸载事件 window.addEventListener('beforeunload', function() { PageViewAI.trackPageLeave(); }); // 监听页面可见性变化 document.addEventListener('visibilitychange', function() { if (document.visibilityState === 'hidden') { PageViewAI.trackPageLeave(); PageViewAI.stopHeartbeat(); } else if (document.visibilityState === 'visible') { PageViewAI.pageStartTime = Date.now(); PageViewAI.startHeartbeat(); } }); // 自动收集性能指标(页面加载完成后) if (document.readyState === 'complete') { // 页面已经加载完成,延迟收集性能指标 setTimeout(function() { PageViewAI.sendPerformanceMetrics(); }, 1000); } else { // 等待页面加载完成 window.addEventListener('load', function() { setTimeout(function() { PageViewAI.sendPerformanceMetrics(); }, 1000); }); } // 暴露到全局 window.PageViewAI = PageViewAI; // 保存原有的队列(如果存在) var queue = window.pv && window.pv.q ? window.pv.q : []; console.log('Found queued commands:', queue); // 设置 pv 函数 window.pv = function() { var args = Array.prototype.slice.call(arguments); var command = args[0]; console.log('pv function called with:', args); switch(command) { case 'init': PageViewAI.init(args[1], args[2]); // 初始化后启动心跳 PageViewAI.startHeartbeat(); break; case 'send': if (args[1] === 'pageview') { PageViewAI.trackPageview(); } else if (args[1] === 'event') { PageViewAI.trackEvent(args[2], args[3]); } else if (args[1] === 'performance') { PageViewAI.sendPerformanceMetrics(); } break; case 'performance': // 手动触发性能指标收集 PageViewAI.sendPerformanceMetrics(); break; default: console.warn('PageViewAI: Unknown command', command); } }; // 处理队列中的命令 console.log('Processing', queue.length, 'queued commands'); for (var i = 0; i < queue.length; i++) { console.log('Processing queued command:', queue[i]); window.pv.apply(null, queue[i]); } })();