1. 项目概述:一个为开发者量身定制的光标使用追踪器
如果你是一名开发者,尤其是深度依赖 Cursor 这类 AI 驱动的代码编辑器的开发者,你是否有过这样的困惑:我每天在编辑器里到底花了多少时间?我使用 AI 补全和聊天的频率有多高?哪些项目占用了我的大部分编码时间?这些看似琐碎的数据,恰恰是优化个人工作流、量化工作效率、甚至评估工具 ROI 的关键。ofershap/cursor-usage-tracker这个项目,就是为了解决这些问题而生的。
简单来说,这是一个专门为 Cursor 编辑器设计的本地使用数据追踪工具。它像一个沉默的助手,在后台安静地记录下你在 Cursor 中的每一次按键、每一次 AI 交互、每一次文件切换,并将这些原始数据转化为结构化的、可分析的日志。它的核心价值在于“量化”与“洞察”——将主观的“我感觉今天效率很高”变成客观的“我今天在项目A上进行了 2.5 小时的高频编码,触发了 87 次 AI 补全,其中 65% 被接受”。对于追求效率极致的开发者、希望评估 AI 编码助手真实效能的团队、或是单纯有数据收集癖好的极客来说,这个工具提供了一个绝佳的起点。
项目本身基于 Node.js 开发,通过监听 Cursor 编辑器在特定目录下生成的日志文件来工作。这意味着它完全在本地运行,你的所有编码数据都不会离开你的电脑,在隐私方面让人安心。接下来,我将带你深入拆解这个项目的设计思路、实现细节,并分享如何将其部署到你的日常开发环境中,让它真正成为你提升编码效率的得力参谋。
2. 核心设计思路与架构拆解
2.1 为什么选择监听日志文件?
当我们决定要追踪一个桌面应用的使用情况时,通常有几种思路:一是修改应用本身,注入我们的追踪代码;二是利用操作系统级的 API 进行监控;三是寻找应用自身产生的数据出口。cursor-usage-tracker选择了第三条路,这也是最优雅、侵入性最低的方案。
Cursor 编辑器基于 VS Code,而 VS Code 本身就有完善的日志机制,用于调试和问题诊断。这些日志通常记录了编辑器的大量内部事件,包括窗口活动、扩展生命周期、以及——关键所在——Telemetry(遥测)数据。Telemetry 数据包含了丰富的用户交互信息。该项目的聪明之处在于,它没有尝试去破解或拦截 Cursor 的进程,而是定位到 Cursor 存储这些日志文件的目录(通常是~/Library/Logs/Cursor/或%APPDATA%\Cursor\logs),然后使用 Node.js 的fs.watchAPI 去监听这些日志文件的变动。
这种设计带来了几个显著优势:
- 非侵入性:完全不需要修改 Cursor 的安装文件或运行代码,避免了潜在的稳定性问题和版本兼容性困扰。
- 低开销:文件监听是操作系统提供的高效机制,相比轮询或进程注入,资源消耗极小。
- 安全性:所有操作都在用户权限内,读取的是应用公开生成的日志,不涉及敏感内存操作。
- 可移植性:只要找到对应平台的日志路径,同一套监听逻辑可以轻松适配 Windows、macOS 和 Linux。
2.2 数据流与核心模块解析
整个工具的数据流可以清晰地分为四个阶段:采集、解析、聚合与输出。
第一阶段:采集(Watcher)这是项目的触发器。核心模块会启动一个文件监听器,盯住 Cursor 的日志目录。一旦有新的日志文件被创建(例如,每天 Cursor 可能会启动一个新的日志文件),或现有日志文件被追加了内容,监听器就会立即捕获到这一事件。这里的一个关键技术点是处理文件滚动(Log Rotation)。开发者可能会同时打开多个 Cursor 窗口,或者 Cursor 自身会按大小或日期分割日志,监听器必须能稳定地跟踪到当前正在被写入的活跃日志文件。
第二阶段:解析(Parser)采集到的是原始的、文本格式的日志行。每行日志可能包含时间戳、日志级别、进程ID以及最重要的 JSON 格式的 Telemetry 事件数据。解析模块的任务就是:
- 过滤出包含
telemetry/关键字的行,因为只有这些行才包含我们关心的用户行为数据。 - 从该行中提取出完整的 JSON 字符串。
- 将这个 JSON 字符串反序列化为 JavaScript 对象。
这个环节的挑战在于日志格式的稳定性。虽然 Telemetry 事件的结构大体一致,但不同版本的 Cursor 可能在字段名或数据结构上做微调。一个健壮的解析器需要有一定的容错能力,避免因为某个字段缺失而导致整个进程崩溃。
第三阶段:聚合(Aggregator)原始事件是细粒度的、海量的。例如,一次代码补全的接受,可能对应多个事件:补全列表展示、用户选择、补全应用。聚合模块的作用,就是根据预定义的规则,将这些低级别事件聚合成有业务意义的高级指标。项目核心追踪的几类数据包括:
- 活跃时间:如何定义一次“编码会话”?通常采用“无操作超时”机制,比如设定 5 分钟无任何事件,则认为会话结束。
- AI 交互:区分“补全建议展示”、“补全被接受”、“与 Cursor Chat 对话”等。
- 文件与项目:记录当前活跃的文件路径和所属项目(通常通过工作区或根目录判断)。
第四阶段:输出(Exporter)处理好的数据需要持久化。项目默认可能将数据输出为本地 JSON 文件或 SQLite 数据库。更高级的用法可以集成到自建的数据看板(如 Grafana)或时间追踪工具(如 Toggl Track)中。输出格式的设计决定了后续分析的便利性。
2.3 技术栈选型背后的考量
项目选择了 Node.js 作为实现语言,这是一个非常务实的选择。
- 生态丰富:
fs.watch、tail类库(如tail-file)、JSON 处理、数据库驱动等,在 Node.js 生态中都有成熟、高性能的解决方案。 - 跨平台:Node.js 本身是跨平台的,配合
path模块和简单的平台判断,可以很容易地实现 Windows、macOS、Linux 三端的支持。 - 轻量高效:对于这种 I/O 密集型(主要是文件读取和数据库写入)的后台守护进程,Node.js 的异步非阻塞模型能很好地胜任,保持低内存占用。
数据库方面,选择 SQLite 作为本地存储是黄金标准。它无需安装服务器,单个文件便于管理和备份,并且通过better-sqlite3这类库能提供同步的、高性能的访问,非常适合这种持续写入、偶尔查询的场景。
3. 核心细节解析与实操要点
3.1 日志源定位与跨平台兼容性
第一步,也是最重要的一步,就是告诉程序去哪里找日志。Cursor 的日志路径因操作系统而异:
- macOS:
~/Library/Logs/Cursor/ - Windows:
%APPDATA%\Cursor\logs\(通常对应C:\Users\<YourUsername>\AppData\Roaming\Cursor\logs) - Linux:
~/.config/Cursor/logs/(可能因发行版或安装方式略有不同)
在代码中,我们需要通过process.platform来判断操作系统,然后拼接出正确的路径。这里有个细节:~在 Node.js 的fs模块中不能直接识别,需要先通过os.homedir()方法获取用户主目录的绝对路径。
const os = require('os'); const path = require('path'); function getCursorLogPath() { const homeDir = os.homedir(); switch (process.platform) { case 'darwin': // macOS return path.join(homeDir, 'Library', 'Logs', 'Cursor'); case 'win32': // Windows // APPDATA 环境变量通常更可靠 const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'); return path.join(appData, 'Cursor', 'logs'); case 'linux': return path.join(homeDir, '.config', 'Cursor', 'logs'); default: throw new Error(`Unsupported platform: ${process.platform}`); } }注意:Cursor 的更新可能会改变日志的存储结构或命名规则。在首次运行或更新 Cursor 后,最好手动检查一下上述路径是否存在,以及日志文件的命名模式(例如
YYYYMMDD.log还是exthost.log)。
3.2 关键 Telemetry 事件解析
并非所有日志行都有用。我们需要聚焦于包含telemetry/的事件。以下是一些典型的需要捕获和分析的事件:
telemetry/completionAccepted: 这是核心指标。当用户通过 Tab 或 Enter 键接受一个 AI 代码补全建议时触发。事件体中通常包含measurements字段,里面有acceptedLength(接受的字符数)和providerId(补全提供者,如github.copilot)等信息。telemetry/completionShown: 补全建议列表被展示出来时触发。结合completionAccepted,可以计算补全的“接受率”(Accepted / Shown),这是一个衡量补全质量的关键指标。telemetry/chatSession: 与 Cursor 的内置聊天功能交互时触发。可能会包含会话的起始、消息数量、是否调用了代码编辑功能等。这是追踪 AI 对话使用情况的主要来源。telemetry/editorAction: 各种编辑器动作,如保存文件、格式化、重构等。可以用来分析工作习惯。telemetry/settingsChanged: 设置变更。有助于了解用户如何调优自己的编辑器。
解析时,我们需要编写健壮的代码来处理可能缺失的字段。例如:
function parseTelemetryLine(line) { try { // 1. 检查是否是telemetry事件 if (!line.includes('telemetry/')) { return null; } // 2. 提取JSON部分(通常位于行尾) const jsonStartIndex = line.indexOf('{'); const jsonEndIndex = line.lastIndexOf('}'); if (jsonStartIndex === -1 || jsonEndIndex === -1) { return null; } const jsonStr = line.substring(jsonStartIndex, jsonEndIndex + 1); const event = JSON.parse(jsonStr); // 3. 提取核心信息 return { name: event.name || 'unknown', timestamp: new Date(event.timestamp || Date.now()), properties: event.properties || {}, measurements: event.measurements || {} }; } catch (error) { // 记录解析错误,但不要阻塞进程 console.error('Failed to parse telemetry line:', error.message, line.substring(0, 100)); return null; } }3.3 会话管理与活跃时间计算
如何从离散的事件流中计算出“我今天编码了 3 小时”?这需要定义“活跃会话”。
一个常见的算法是:无操作超时。我们维护一个最后一次活动事件的时间戳lastEventTime。每当收到一个新事件,就更新这个时间戳。同时,启动一个定时器(比如每 60 秒检查一次),如果发现当前时间与lastEventTime的差值超过了预设的阈值(例如5 分钟),则认为上一个活跃会话结束,并开始一个新的会话。
这个阈值需要仔细选择:
- 太短(如 1 分钟):接个电话、回个消息的间隙就会被切分成多个会话,数据碎片化严重。
- 太长(如 30 分钟):中午吃饭休息的时段也会被计入编码时间,高估了实际工作量。
- 5-10 分钟是一个比较合理的范围,平衡了连续性和准确性。
在实现上,我们需要为每个“工作区”或“项目”单独管理会话,因为开发者可能在不同项目间切换。当检测到window或workspace相关的事件变化时,即使时间没超时,也可能需要强制结束当前项目会话并开始新项目会话。
4. 实操部署与核心环节实现
4.1 环境准备与项目初始化
假设你已经安装了 Node.js(建议版本 16+)和 npm/yarn/pnpm。
首先,将项目克隆到本地:
git clone https://github.com/ofershap/cursor-usage-tracker.git cd cursor-usage-tracker安装项目依赖。查看package.json,核心依赖可能包括:
tail-file: 用于高效跟踪日志文件末尾的新增内容,比fs.watch直接读文件更可靠。better-sqlite3: 高性能的 SQLite 驱动,用于本地数据存储。date-fns或moment: 日期时间处理库。yargs或commander: 命令行参数解析。
执行安装命令:
npm install # 或 yarn install # 或 pnpm install4.2 配置与首次运行
通常,项目会提供一个配置文件(如config.json或config.js)或支持命令行参数。你需要配置的关键项有:
- 日志路径: 如果自动检测失败,可以手动指定。
- 会话超时时间: 根据个人习惯调整
sessionTimeoutMinutes。 - 输出方式: 是输出到 SQLite 文件,还是 JSON 文件,或者同时输出。
- 忽略的事件/项目: 你可能不想追踪某些特定项目(如临时文件夹)或某些噪音事件。
创建一个简单的config.json:
{ "sessionTimeoutMinutes": 5, "databasePath": "./cursor_usage.db", "logPath": null, // null 表示自动检测 "ignoredWorkspaces": ["/tmp", "/private/var"] }然后,以守护进程的方式启动追踪器。最朴素的方式是使用node直接运行主文件:
node index.js --config ./config.json为了让它在后台持续运行,特别是在服务器或长期开机的开发机上,建议使用进程管理工具:
- pm2(推荐):
pm2 start index.js --name cursor-tracker - systemd(Linux): 创建一个 systemd service 文件。
- LaunchAgent(macOS): 创建一个
.plist文件放入~/Library/LaunchAgents/。 - 任务计划程序(Windows): 创建一个开机启动的任务。
使用 pm2 还能方便地查看日志和管理进程状态:
pm2 logs cursor-tracker --lines 100 # 查看最近100行日志 pm2 monit # 监控资源占用 pm2 save && pm2 startup # 设置开机自启4.3 数据存储与表结构设计
使用 SQLite 存储,我们需要设计合理的表结构。以下是一个基础的设计方案:
表1:sessions(编码会话表)
| 字段名 | 类型 | 说明 |
|---|---|---|
id | INTEGER PRIMARY KEY | 自增主键 |
start_time | DATETIME | 会话开始时间 |
end_time | DATETIME | 会话结束时间 |
duration_seconds | INTEGER | 会话时长(秒) |
project_path | TEXT | 项目根目录路径 |
editor_version | TEXT | Cursor 版本号 |
表2:events(原始事件表)
| 字段名 | 类型 | 说明 |
|---|---|---|
id | INTEGER PRIMARY KEY | 自增主键 |
session_id | INTEGER | 关联的会话ID |
event_time | DATETIME | 事件发生时间 |
event_name | TEXT | 事件名称,如telemetry/completionAccepted |
properties | TEXT | 事件属性(存储为JSON字符串) |
measurements | TEXT | 测量值(存储为JSON字符串) |
表3:daily_summary(每日汇总表,可物化视图或定时生成)
| 字段名 | 类型 | 说明 |
|---|---|---|
date | DATE PRIMARY KEY | 日期 |
total_active_seconds | INTEGER | 当日总活跃秒数 |
completion_shown | INTEGER | 补全展示次数 |
completion_accepted | INTEGER | 补全接受次数 |
acceptance_rate | REAL | 接受率 (accepted/shown) |
chat_interactions | INTEGER | 聊天交互次数 |
files_opened | INTEGER | 打开过的不同文件数 |
在代码中,使用better-sqlite3初始化数据库和表:
const Database = require('better-sqlite3'); const db = new Database(config.databasePath); db.exec(` CREATE TABLE IF NOT EXISTS sessions (...); CREATE TABLE IF NOT EXISTS events (...); CREATE TABLE IF NOT EXISTS daily_summary (...); `);每次解析到一个有效事件后,就将其插入events表,并更新当前活跃会话的信息。当会话超时结束时,将完整的会话记录插入sessions表。
4.4 基础数据分析与可视化
数据存好了,如何看?最简单直接的方式是写一些 SQL 查询,或者用 Node.js 写个小脚本输出报告。
示例1:查询今日编码时间
SELECT strftime('%H:%M', time(start_time, 'unixepoch')) as start, strftime('%H:%M', time(end_time, 'unixepoch')) as end, duration_seconds / 60 as duration_minutes, project_path FROM sessions WHERE date(start_time) = date('now') ORDER BY start_time;示例2:生成本周每日工作概览
SELECT date(start_time) as day, SUM(duration_seconds) / 3600.0 as total_hours, COUNT(DISTINCT project_path) as project_count FROM sessions WHERE start_time >= date('now', 'weekday 0', '-7 days') -- 本周一 GROUP BY day ORDER BY day;对于更喜欢图形界面的开发者,可以将 SQLite 数据连接到可视化工具:
- Metabase: 开源 BI 工具,连接 SQLite 文件后可以轻松拖拽创建图表和仪表盘。
- Grafana + SQLite Plugin: 虽然 Grafana 原生对 SQLite 支持不强,但可以通过第三方插件或将数据定期导出到其他数据库(如 PostgreSQL)来实现。
- 使用 Python (Pandas + Matplotlib/Seaborn): 写一个脚本定期读取 SQLite,生成静态的 HTML 报告或图片,通过邮件或聊天机器人发送给自己。
一个快速生成简单柱状图(显示一周内每天接受补全的数量)的 Python 示例:
import sqlite3 import pandas as pd import matplotlib.pyplot as plt conn = sqlite3.connect('./cursor_usage.db') df = pd.read_sql_query(""" SELECT date(start_time) as date, COUNT(*) as accepted_count FROM events WHERE event_name = 'telemetry/completionAccepted' AND date(start_time) >= date('now', '-7 days') GROUP BY date ORDER BY date """, conn) conn.close() df.plot(kind='bar', x='date', y='accepted_count', legend=False) plt.title('Daily Accepted Completions (Last 7 Days)') plt.xlabel('Date') plt.ylabel('Count') plt.tight_layout() plt.savefig('./weekly_report.png') plt.show()5. 常见问题与排查技巧实录
在实际部署和使用cursor-usage-tracker的过程中,你几乎一定会遇到一些问题。下面是我在搭建和使用类似工具时踩过的坑和总结的解决方案。
5.1 数据抓取不到或不全
这是最常见的问题。表现为数据库里没有数据,或者数据量远小于预期。
可能原因1:日志路径错误
- 排查:首先确认 Cursor 正在运行,然后手动前往上一节提到的平台相关路径,查看是否存在
.log文件,并检查其是否有新内容写入。 - 解决:在配置文件中硬编码正确的绝对路径。确保追踪器进程有该目录的读取权限。
- 排查:首先确认 Cursor 正在运行,然后手动前往上一节提到的平台相关路径,查看是否存在
可能原因2:Cursor 日志级别设置过低
- 排查:Cursor 默认可能不会输出
telemetry级别的日志到文件。你需要检查 Cursor 的设置。 - 解决:在 Cursor 中,通过命令面板 (
Cmd/Ctrl+Shift+P) 输入Developer: Set Log Level...,将其设置为Trace或Debug。这能确保最详细的事件被记录到日志文件。注意,这可能会增加日志文件的大小。
- 排查:Cursor 默认可能不会输出
可能原因3:文件监听失效
- 排查:Node.js 的
fs.watch在某些系统(尤其是使用网络驱动器或虚拟机)上可能不可靠。监听器可能因为文件重命名(日志轮转)而丢失跟踪。 - 解决:
- 使用更健壮的库,如
chokidar,它封装了原生 API 并提供了更好的兼容性和更多功能。 - 采用“
tail -f”模式,即始终读取当前日志文件的末尾。使用tail-file或node-tail这类库,它们专门为跟踪日志文件末尾新增内容而设计,能更好地处理文件轮转。
const TailFile = require('@logdna/tail-file'); const tail = new TailFile('/path/to/cursor.log'); tail.on('data', (data) => { // 处理新增的每一行数据 const lines = data.toString().split('\n'); lines.forEach(processLine); }); tail.start().catch((err) => console.error('TailFile error:', err)); - 使用更健壮的库,如
- 排查:Node.js 的
5.2 数据库锁或性能问题
当事件频率很高时,频繁的数据库写入可能成为瓶颈。
可能原因1:同步写入导致事件堆积
- 现象:进程 CPU 或内存占用变高,数据库文件所在磁盘 I/O 繁忙。
- 解决:采用批量插入和异步写入策略。不要每收到一个事件就执行一次
INSERT。可以设置一个缓冲区(数组),当缓冲区达到一定大小(如 100 条事件)或经过一定时间(如 5 秒)后,一次性执行批量插入。
let eventBuffer = []; const BUFFER_LIMIT = 100; const FLUSH_INTERVAL_MS = 5000; function addEventToBuffer(event) { eventBuffer.push(event); if (eventBuffer.length >= BUFFER_LIMIT) { flushBufferToDB(); } } setInterval(flushBufferToDB, FLUSH_INTERVAL_MS); function flushBufferToDB() { if (eventBuffer.length === 0) return; const toInsert = eventBuffer; eventBuffer = []; // 使用事务进行批量插入,速度极快 const insertStmt = db.prepare('INSERT INTO events (...) VALUES (?,?,?,?,?)'); const insertMany = db.transaction((events) => { for (const event of events) { insertStmt.run(event.sessionId, event.time, event.name, ...); } }); insertMany(toInsert); }可能原因2:SQLite 被其他进程锁定
- 现象:偶尔出现
SQLITE_BUSY错误。 - 解决:
- 确保你的读写操作都使用了
better-sqlite3的WAL(Write-Ahead Logging) 模式,它支持更高的并发读和写。在初始化数据库时启用:new Database('file.db', { readonly: false, fileMustExist: false, timeout: 5000, verbose: null });并执行PRAGMA journal_mode = WAL;。 - 为数据库操作设置合理的超时(
timeout选项)。 - 如果你的可视化工具(如 Metabase)也在同时读取数据库,WAL 模式通常能很好地处理这种读写并发。
- 确保你的读写操作都使用了
- 现象:偶尔出现
5.3 数据解读与准确性困惑
数据有了,但怎么看?怎么保证它反映真实情况?
问题:活跃时间比我感觉的长很多
- 分析:这很可能是因为“无操作超时”阈值设置得太长。如果你习惯在思考时长时间不操作编辑器,或者开着编辑器去做其他事情,这些时间都会被计入。
- 调整:将
sessionTimeoutMinutes从 5 分钟调低至 2-3 分钟。但这可能会将一些短暂的思考停顿切割成多个会话。没有完美的值,需要根据个人工作模式权衡。更好的办法是,在分析时,可以过滤掉过短的会话(如少于 30 秒的会话),这些可能是误触发或无效操作。
问题:补全接受率异常低或高
- 分析:接受率受多种因素影响:代码复杂度、个人编码风格、补全模型质量、甚至当天状态。单独看某一天的数据可能波动很大。
- 建议:不要过度解读单点数据。关注长期趋势(如周平均、月平均)。如果接受率持续低于 20%,也许可以反思一下:是否过于依赖补全,导致它经常给出不相关的建议?或者你的编码上下文信息给得不足?反过来,如果接受率超过 70%,说明 AI 补全与你当前的工作流契合度非常高。
问题:如何区分不同项目的工作量?
- 分析:原始事件中可能只包含文件路径。需要从文件路径推断出项目根目录。
- 实现:一个简单的启发式方法是:维护一个“项目根目录”列表。当打开一个文件时,向上级目录查找是否存在
.git文件夹、package.json、pyproject.toml等标志性文件,第一个找到的目录即视为项目根。可以将这个逻辑实现在会话管理模块中,为每个事件或会话打上project_root标签。
5.4 隐私与数据安全考量
所有数据都在本地,这是最大的安全保障。但仍需注意:
- 日志文件本身:Cursor 的日志文件可能包含你打开的文件路径、甚至代码片段(如果日志级别设得很高)。确保你的追踪器只读取而不修改它们,并且处理后的数据库文件也存放在安全的位置。
- 数据库文件:你生成的 SQLite 数据库包含了你的工作习惯分析。不要将其上传到不安全的云存储或公开的代码仓库。如果需要进行远程分析(如在公司内部分享团队效率数据),务必先对数据进行匿名化聚合,移除所有个人可识别信息(如具体的文件路径、项目名称),只保留脱敏后的统计指标。
- 长期运行:作为守护进程,确保它不会因为内存泄漏或异常而崩溃。使用
pm2等工具可以配置内存上限和异常重启策略。
最后,这个工具的价值不在于监控,而在于反思和优化。定期(比如每周五下午)花 10 分钟看看自己的数据报告:哪个项目耗时最多?哪个时段的编码效率最高?AI 聊天是用来解决具体问题多,还是用来生成样板代码多?基于这些洞察,你可以有意识地调整自己的工作习惯、工具配置,甚至项目时间安排,这才是数据驱动个人成长的真正开始。