authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-08 02:26:27-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-08 02:26:27-07:00
log9381533771ef50d58304f79fac2b74ebd8932be7
tree77b0069272338ce54714d53f2c08c10547fe841a
parent13c5328dd02ab0f5863f7d46455efc8dc243d4ce
signature Signed by PGP key B5690EEEBB952194

fix(usage): Cursor account history loads about 4x faster (#17140)

Keep up to six Cursor usage pages requested ahead of the one being read, still read in page order, and cancel requests left in flight when the read ends. Continues #14384. Co-authored-by: Krishna Vijay <228381532+im-kvijay@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 92 insertions(+), 2 deletions(-)

apps/server/src/usage/cursorUsageReader.test.ts+71
...@@ -19,4 +19,75 @@ describe("readCursorAccountUsage", () => {...@@ -19,4 +19,75 @@ describe("readCursorAccountUsage", () => {
19 error: "Allow Keychain access on the Mac running T3 Code, then refresh.",19 error: "Allow Keychain access on the Mac running T3 Code, then refresh.",
20 });20 });
21 });21 });
22
23 it("reads the pages behind the first together and keeps them in page order", async () => {
24 const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`;
25 let inFlight = 0;
26 let mostInFlight = 0;
27 const result = await readCursorAccountUsage(
28 { kind: "keychain" },
29 0,
30 1781000000000,
31 async (_url, init) => {
32 const page: number = JSON.parse(String(init.body)).page;
33 inFlight++;
34 mostInFlight = Math.max(mostInFlight, inFlight);
35 // Later pages answer first, so record order cannot come from arrival order.
36 for (let turn = page; turn < 9; turn++) await Promise.resolve();
37 inFlight--;
38 return Response.json({
39 totalUsageEventsCount: 8001,
40 usageEventsDisplay: Array.from({ length: page === 9 ? 1 : 1000 }, (_, index) => ({
41 timestamp: String(1780000000000 + (page - 1) * 1000 + index),
42 model: "gpt-5",
43 tokenUsage: { inputTokens: 1 },
44 })),
45 });
46 },
47 async () => accessToken,
48 );
49 assert.isNull(result.error);
50 assert.strictEqual(mostInFlight, 6);
51 assert.deepStrictEqual(
52 result.records.map((record) => record.timestampMs),
53 Array.from({ length: 8001 }, (_, index) => 1780000000000 + index),
54 );
55 });
56
57 it("ends like a page-by-page read when a page fails, leaving no request open", async () => {
58 const accessToken = `header.${Buffer.from(JSON.stringify({ sub: "auth|demo" })).toString("base64url")}.signature`;
59 const fullPage = {
60 totalUsageEventsCount: 8001,
61 usageEventsDisplay: Array.from({ length: 1000 }, () => ({ tokenUsage: null })),
62 };
63 // Page 3 fails while the pages after it are still waiting on Cursor.
64 for (const [secondPage, error] of [
65 [() => Response.json(fullPage), "Cursor account usage could not be read."],
66 [() => new Response(null, { status: 401 }), "Sign in to Cursor again to read account usage."],
67 ] as const) {
68 let open = 0;
69 const result = await readCursorAccountUsage(
70 { kind: "keychain" },
71 0,
72 1781000000000,
73 (_url, init) => {
74 const page: number = JSON.parse(String(init.body)).page;
75 if (page === 1) return Promise.resolve(Response.json(fullPage));
76 if (page === 2) return Promise.resolve(secondPage());
77 if (page === 3) return Promise.reject(new Error("connection reset"));
78 open++;
79 return new Promise((_resolve, reject) => {
80 init.signal?.addEventListener("abort", () => {
81 open--;
82 reject(init.signal?.reason);
83 });
84 });
85 },
86 async () => accessToken,
87 );
88 assert.strictEqual(result.error, error);
89 assert.deepStrictEqual(result.records, []);
90 assert.strictEqual(open, 0);
91 }
92 });
22});93});
apps/server/src/usage/cursorUsageReader.ts+21-2
...@@ -106,6 +106,7 @@ export async function readCursorAccountUsage(...@@ -106,6 +106,7 @@ export async function readCursorAccountUsage(
106 };106 };
107 }107 }
108 let accountKey: string | null = null;108 let accountKey: string | null = null;
109 const cancel = new AbortController();
109 try {110 try {
110 const payload = accessToken.split(".")[1];111 const payload = accessToken.split(".")[1];
111 const subject = object(112 const subject = object(
...@@ -117,14 +118,14 @@ export async function readCursorAccountUsage(...@@ -117,14 +118,14 @@ export async function readCursorAccountUsage(
117 accountKey = accountHash(subject);118 accountKey = accountHash(subject);
118 if (!Number.isFinite(sinceMs) || !Number.isFinite(endDate) || sinceMs < 0 || sinceMs > endDate)119 if (!Number.isFinite(sinceMs) || !Number.isFinite(endDate) || sinceMs < 0 || sinceMs > endDate)
119 throw new Error("Invalid date window");120 throw new Error("Invalid date window");
120 const deadline = AbortSignal.timeout(60_000);121 const deadline = AbortSignal.any([cancel.signal, AbortSignal.timeout(60_000)]);
121 const records: UsageRecord[] = [];122 const records: UsageRecord[] = [];
122 const occurrences = new Map<string, number>();123 const occurrences = new Map<string, number>();
123 const pages: unknown[][] = [];124 const pages: unknown[][] = [];
124 let completed = false;125 let completed = false;
125 const pageSize = 1000;126 const pageSize = 1000;
126 let total: number | undefined;127 let total: number | undefined;
127 for (let page = 1; ; page++) {128 const readPage = async (page: number) => {
128 // A count can include overlapping page boundaries. Allow room to129 // A count can include overlapping page boundaries. Allow room to
129 // reconcile them without imposing a fixed account-size limit.130 // reconcile them without imposing a fixed account-size limit.
130 if (page > (total === undefined ? 1000 : Math.ceil(total / pageSize) * 2 + 1)) {131 if (page > (total === undefined ? 1000 : Math.ceil(total / pageSize) * 2 + 1)) {
...@@ -181,6 +182,21 @@ export async function readCursorAccountUsage(...@@ -181,6 +182,21 @@ export async function readCursorAccountUsage(
181 throw new Error("Inconsistent account usage page");182 throw new Error("Inconsistent account usage page");
182 }183 }
183 if (typeof count === "number") total = count;184 if (typeof count === "number") total = count;
185 return events;
186 };
187 // Cursor caps a page at 1,000 events and takes about a second to answer one, so the pages
188 // the first one's count implies are requested up to six ahead and consumed in page order.
189 const ahead: ReturnType<typeof readPage>[] = [];
190 for (let page = 1; ; page++) {
191 while (ahead.length < Math.min(6, Math.ceil((total ?? 0) / pageSize) - page + 1)) {
192 const pending = readPage(page + ahead.length);
193 // Handled when its turn comes, or cancelled if the read ends first.
194 pending.catch(() => undefined);
195 ahead.push(pending);
196 }
197 const events = await (ahead.shift() ?? readPage(page));
198 // A rejected login comes back as the finished result instead of a page.
199 if (!Array.isArray(events)) return events;
184 pages.push(events);200 pages.push(events);
185 if (events.length < pageSize) {201 if (events.length < pageSize) {
186 completed = true;202 completed = true;
...@@ -268,5 +284,8 @@ export async function readCursorAccountUsage(...@@ -268,5 +284,8 @@ export async function readCursorAccountUsage(
268 missing: false,284 missing: false,
269 error: "Cursor account usage could not be read.",285 error: "Cursor account usage could not be read.",
270 };286 };
287 } finally {
288 // An early exit leaves the pages requested ahead of it in flight.
289 cancel.abort();
271 }290 }
272}291}