You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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());
}

关键说明

  1. 用chrome.tabs.onActivated替代单纯监听highlighted状态,更精准捕捉标签页焦点切换,避免状态遗漏
  2. 所有查找标签页的操作都先做存在性校验,彻底避免tabExisting为null的报错
  3. 新增hostname字段,方便后续按站点统计数据
  4. 小时统计通过时间字符串分组,可灵活扩展到天、周等维度

内容的提问来源于stack exchange,提问作者LeCoda

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 17:45:32