Chrome扩展开发:利用Tabs API统计标签页停留时长与切换频率
Chrome标签页使用数据统计方案
针对你遇到的tabExisting频繁为null的问题,以及小时维度统计需求,以下是可行的实现方案:
一、核心问题修复:避免tabExisting为null
你之前的逻辑中tabExisting为null,大概率是因为:
- 当前标签页已被关闭,但
currentPages未及时清理 - 从
currentPages查找对应tabId的项时未做存在性校验
解决思路:
- 新增标签页关闭事件监听,及时标记标签页结束时间并移出现有列表
- 每次查找
tabExisting时先判断是否存在,再进行状态对比
二、完整的标签页数据追踪实现
通过Chrome Tabs API的多个事件组合,精准追踪标签页的创建、激活、更新、关闭全生命周期:
export type TAB_USAGE = { tabId: number; index?: number; highlighted: boolean; url: string; startTime: string; endTime?: string; startFocus?: string; endFocus?: string; hostname: string; // 新增hostname字段,方便后续统计 }; const getHostname = (url: string) => { try { return new URL(url).hostname; } catch (e) { return 'unknown'; // 处理无效URL情况 } }; // 辅助函数:获取当前时间字符串(ISO格式) const getCurrentTime = () => new Date().toISOString(); // 辅助函数:查找当前列表中的标签页 const findTabInList = (tabId: number, list: TAB_USAGE[]) => { return list.find(item => item.tabId === tabId); }; export function enableTabListener( socket: any, currentPages: TAB_USAGE[], visitedPages: Partial<TAB_USAGE>[] ) { // 全局变量:记录标签页切换次数 let totalTabSwitches = 0; // 记录每次切换的时间,用于小时维度统计 const switchHistory: { time: string }[] = []; // 监听标签页创建 chrome.tabs.onCreated.addListener((tab) => { if (!tab.url) return; // 忽略未加载完成的标签页 const tabUsage: TAB_USAGE = { tabId: tab.id!, index: tab.index, highlighted: tab.highlighted, url: tab.url, startTime: getCurrentTime(), hostname: getHostname(tab.url), startFocus: tab.highlighted ? getCurrentTime() : undefined }; currentPages.push(tabUsage); }); // 监听标签页激活(焦点切换) chrome.tabs.onActivated.addListener((activeInfo) => { totalTabSwitches++; switchHistory.push({ time: getCurrentTime() }); // 先标记之前激活的标签页结束焦点 const prevActiveTab = currentPages.find(item => item.highlighted); if (prevActiveTab) { prevActiveTab.endFocus = getCurrentTime(); prevActiveTab.highlighted = false; // 计算停留时长并同步到visitedPages if (prevActiveTab.startFocus) { const duration = new Date(prevActiveTab.endFocus).getTime() - new Date(prevActiveTab.startFocus).getTime(); const visitedItem = visitedPages.find(item => item.tabId === prevActiveTab.tabId); if (visitedItem) { (visitedItem as any).totalFocusDuration = (visitedItem as any).totalFocusDuration || 0 + duration; } else { visitedPages.push({ ...prevActiveTab, totalFocusDuration: duration }); } } } // 标记新激活的标签页开始焦点 const currentTab = findTabInList(activeInfo.tabId, currentPages); if (currentTab) { currentTab.highlighted = true; currentTab.startFocus = getCurrentTime(); currentTab.endFocus = undefined; } }); // 监听标签页更新(URL变化、状态变化) chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { const tabExisting = findTabInList(tabId, currentPages); if (!tabExisting) return; // 避免null报错 // 更新URL和hostname if (changeInfo.url && tab.url) { tabExisting.url = tab.url; tabExisting.hostname = getHostname(tab.url); } // 兼容特殊场景下的highlighted状态变化 if (changeInfo.highlighted !== undefined && tabExisting.highlighted !== changeInfo.highlighted) { tabExisting.highlighted = changeInfo.highlighted; if (changeInfo.highlighted) { tabExisting.startFocus = getCurrentTime(); tabExisting.endFocus = undefined; } else { tabExisting.endFocus = getCurrentTime(); // 同步时长到visitedPages if (tabExisting.startFocus) { const duration = new Date(tabExisting.endFocus).getTime() - new Date(tabExisting.startFocus).getTime(); const visitedItem = visitedPages.find(item => item.tabId === tabExisting.tabId); if (visitedItem) { (visitedItem as any).totalFocusDuration = (visitedItem as any).totalFocusDuration || 0 + duration; } else { visitedPages.push({ ...tabExisting, totalFocusDuration: duration }); } } } } }); // 监听标签页关闭 chrome.tabs.onRemoved.addListener((tabId) => { const tabIndex = currentPages.findIndex(item => item.tabId === tabId); if (tabIndex === -1) return; const closedTab = currentPages.splice(tabIndex, 1)[0]; closedTab.endTime = getCurrentTime(); // 如果标签页关闭时处于激活状态,补全endFocus if (closedTab.highlighted && closedTab.startFocus) { closedTab.endFocus = getCurrentTime(); const duration = new Date(closedTab.endFocus).getTime() - new Date(closedTab.startFocus).getTime(); visitedPages.push({ ...closedTab, totalFocusDuration: duration }); } else { visitedPages.push(closedTab); } }); // 定时上报数据 setInterval(async () => { console.log('sent tab data:', visitedPages); // 这里可以调用socket发送数据 // await socket.emit('tab-data', visitedPages); }, 50000); }
三、小时维度统计实现
通过对visitedPages和切换历史数据按小时分组,统计每个小时内的核心指标:
// 按小时分组统计数据结构 type HourlyStats = { hour: string; // 格式:YYYY-MM-DD HH totalTabsCreated: number; // 新建标签页次数 totalTabSwitches: number; // 标签页切换次数 totalFocusDuration: number; // 总停留时长(毫秒) siteStats: Record<string, { // 各站点统计 duration: number; tabCount: number; }>; }; // 辅助函数:提取时间的小时维度键 const getHourKey = (timeStr: string) => { const date = new Date(timeStr); return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}`; }; export function getHourlyStats(visitedPages: Partial<TAB_USAGE>[], switchHistory: { time: string }[]): HourlyStats[] { const statsMap = new Map<string, HourlyStats>(); // 处理标签页创建和停留时长统计 visitedPages.forEach(page => { if (!page.startTime) return; const hourKey = getHourKey(page.startTime); // 初始化该小时的统计项 if (!statsMap.has(hourKey)) { statsMap.set(hourKey, { hour: hourKey, totalTabsCreated: 0, totalTabSwitches: 0, totalFocusDuration: 0, siteStats: {} }); } const stats = statsMap.get(hourKey)!; // 统计新建标签页次数 stats.totalTabsCreated += 1; // 统计总停留时长和站点数据 if (page.startFocus && page.endFocus) { const duration = new Date(page.endFocus).getTime() - new Date(page.startFocus).getTime(); stats.totalFocusDuration += duration; const hostname = (page as TAB_USAGE).hostname || 'unknown'; if (!stats.siteStats[hostname]) { stats.siteStats[hostname] = { duration: 0, tabCount: 0 }; } stats.siteStats[hostname].duration += duration; stats.siteStats[hostname].tabCount += 1; } }); // 处理标签页切换次数统计 switchHistory.forEach(switchRecord => { const hourKey = getHourKey(switchRecord.time); const stats = statsMap.get(hourKey); if (stats) { stats.totalTabSwitches += 1; } }); return Array.from(statsMap.values()); }
关键说明
- 用
chrome.tabs.onActivated替代单纯监听highlighted状态,更精准捕捉标签页焦点切换,避免状态遗漏 - 所有查找标签页的操作都先做存在性校验,彻底避免
tabExisting为null的报错 - 新增
hostname字段,方便后续按站点统计数据 - 小时统计通过时间字符串分组,可灵活扩展到天、周等维度
内容的提问来源于stack exchange,提问作者LeCoda
相关产品推荐
相关产品推荐

