MoMoYu Blog

摸摸鱼研究局

macOS 如何记录不同网页的使用时间

摸摸鱼的网页时长统计原理

上周更新的 V1.1 版摸摸鱼终于加入了大家期望已久的网页时长记录功能(虽然还不完美),之前就提过这个功能其实还挺难做的,技术方案有不少可选项但都各有利弊,比如说很多人的第一反应(包括我自己)靠浏览器插件做。所以这篇就聊一聊摸摸鱼最后采用的方案细节,是一个挺「克制」的方法——不进浏览器、不碰网页,只在 macOS 系统这一层做判断,简单一句话概括就是:

在支持 macOS 系统级 Automation 的浏览器上通过 AppleScript 读取它当前激活标签页的 URL


详细原理

摸摸鱼内部其实有两层统计:

第一层:记录当前前台应用

App 本身会持续关注当前前台 app 是谁,如果你从 VS Code 切到 Chrome,再切到 Slack,它就会把这几段时间分别累计给不同应用。

第二层:如果当前 app 是浏览器,再细分到网页

如果当前前台 app 是支持的浏览器,网页追踪器就会每隔一小段时间检查一次当前激活的 tab,现在代码里这个轮询周期是 2 秒,以一个比较轻量的频率去确认「你还在不在这个网页上」。

一旦发现:

  • 浏览器不在前台了
  • 当前标签页 URL 变了
  • 当前页面在摸摸鱼里被设置成「忽略」

就会把「上一个网页从开始到现在停留了多久」结算掉,把这段时长累加到对应域名上,本质上这是一种基于边界结算的计时方式,而不是埋点式事件流。

为什么最后按域名统计,而不是按完整 URL?

如果按完整 URL 统计,数据会碎得非常厉害,产品上很难查看各种不同的子域名甚至各种 query 参数,同时也不符合摸摸鱼这种「在精确和简单当中更偏向后者的基础逻辑」。所以最后会把 URL 归一化成域名,例如 docs.github.comwww.github.com 这种层级最后会落到 github.com 上,同时浏览器本体的总时长也会被排除,让网页统计和其他 app 处在同一个层级上,不会出现双重统计。

URL 不是看到就记

为了让结果可用,代码里会主动过滤掉一大批 URL,比如:

  • file:// 本地文件
  • data: 这种内嵌内容
  • chrome://edge://about: 这种浏览器内部页面
  • javascript:devtools://
  • http/https 的地址
  • 一些异常超长 URL

为什么没法做到支持所有浏览器?

既然 Chrome 能拿到当前 URL,为什么 Arc、Dia、Firefox 不行?除了 Firefox 内核都一样?答案很简单:不是所有浏览器都愿意或支持把当前激活标签页 URL 暴露给 macOS 的 Automation 和 AppleScript。我测试下来发觉其实 Chromium 内核的浏览器这部分的能力和保留的功能其实都差不多(可能也是个小地方,大家都懒得改了),唯独 Arc 和 Dia 这同源的两兄弟是整个把 Automation 能力拿掉了,而 Firefox 我只能理解它为了隐私?因为支持的功能还挺多的,唯独拿不到 tab 的 URL。

所以摸摸鱼现在本质是一个「浏览器白名单支持模型」,不是所有浏览器都能自动兼容。某种意义上,这也是做桌面工具一个有意思但挺现实的地方:你不是只和自己的代码打交道,你还要和操作系统以及第三方应用权限打交道。另外,这个 Automation 权限的使用多少有点儿邪修,感觉上 App Store 应该是没希望了哈哈哈哈~

这套方案有什么优点?

站在开发者角度,我自己比较满意的地方:

  1. 不需要浏览器插件 这让整体体验更轻一点,用户不用再额外装一个浏览器 extension,我也不用面对多浏览器、多账户、多端同步带来的开发复杂度。

  2. 统计口径比较符合「真实前台注意力」 虽然依然存在同一个主域名里很难区分专注/摸鱼两种状态,但总体来说这个统计口径与设置方式和 app 时长统计是拉平的,在 UI 上更容易让人理解

  3. 隐私边界更克制 至少在设计上,它不是奔着「采尽可能多的数据」去的,更偏向「为时间分析拿到刚刚够用的信息」。


最后

做摸摸鱼这类产品,会遇到一个有趣的三角矛盾:

  1. 希望统计足够准确
  2. 希望体验足够轻、克制
  3. 又不想碰太多隐私

所以在网页时长统计这件事上,最后选择的是这种我觉得比较平衡的方案:能大致回答「时间花在哪」,但不去做过度采集,也诚实地承认它的边界和限制。如果你刚好也对这类时间统计产品、macOS Automation 感兴趣,希望这篇文章能给你一点启发,如果你是摸摸鱼的用户,也希望这篇文章能让你更放心一点:摸摸鱼确实在记录时间并且不可避免地接触到一些隐私,但我也很在意这件事应该停在哪里。

评论