Skip to content

Commit 3b2f9ee

Browse files
authored
Capture more events when backfilling (#133)
1 parent a989449 commit 3b2f9ee

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

apps/web/src/app/api/events/backfill/__tests__/route.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,15 @@ describe('/api/events/backfill', () => {
239239
}>;
240240

241241
expect(eventData).toHaveLength(1);
242+
242243
expect(eventData[0]).toMatchObject({
243244
taskId: 'test-task-from-file',
244245
type: 'Task Created',
245246
mode: 'code',
246247
userId: 'test-user-id',
247248
orgId: 'test-org-id',
248249
});
250+
249251
expect(typeof eventData[0]?.timestamp).toBe('number');
250252

251253
const dbResults = await analytics.query({
@@ -276,6 +278,7 @@ describe('/api/events/backfill', () => {
276278
expect(dbData).toHaveLength(messages.length);
277279

278280
const textMessage = dbData.find((msg) => msg.say === 'text');
281+
279282
expect(textMessage).toMatchObject({
280283
taskId: 'test-task-from-file',
281284
mode: 'code',
@@ -285,6 +288,7 @@ describe('/api/events/backfill', () => {
285288
});
286289

287290
const apiMessage = dbData.find((msg) => msg.say === 'api_req_started');
291+
288292
expect(apiMessage).toMatchObject({
289293
taskId: 'test-task-from-file',
290294
mode: 'code',
@@ -301,6 +305,134 @@ describe('/api/events/backfill', () => {
301305
expect(messageTypes).toContain('checkpoint_saved');
302306
expect(messageTypes).toContain('reasoning');
303307
expect(messageTypes).toContain('completion_result');
308+
309+
const llmEventsResults = await analytics.query({
310+
query: `
311+
SELECT
312+
taskId,
313+
type,
314+
mode,
315+
inputTokens,
316+
outputTokens,
317+
cacheReadTokens,
318+
cacheWriteTokens,
319+
cost,
320+
userId,
321+
orgId,
322+
timestamp
323+
FROM events
324+
WHERE taskId = 'test-task-from-file' AND type = 'LLM Completion'
325+
ORDER BY timestamp ASC
326+
`,
327+
format: 'JSONEachRow',
328+
});
329+
330+
const llmEventData = (await llmEventsResults.json()) as Array<{
331+
taskId: string;
332+
type: string;
333+
mode: string;
334+
inputTokens: number;
335+
outputTokens: number;
336+
cacheReadTokens?: number;
337+
cacheWriteTokens?: number;
338+
cost?: number;
339+
userId: string;
340+
orgId: string;
341+
timestamp: number;
342+
}>;
343+
344+
expect(llmEventData).toHaveLength(3);
345+
346+
expect(llmEventData[0]).toMatchObject({
347+
taskId: 'test-task-from-file',
348+
type: 'LLM Completion',
349+
mode: 'code',
350+
inputTokens: 12990,
351+
outputTokens: 559,
352+
cacheReadTokens: 0,
353+
cacheWriteTokens: 0,
354+
cost: 0.02246925,
355+
userId: 'test-user-id',
356+
orgId: 'test-org-id',
357+
});
358+
359+
expect(llmEventData[1]).toMatchObject({
360+
taskId: 'test-task-from-file',
361+
type: 'LLM Completion',
362+
mode: 'code',
363+
inputTokens: 13788,
364+
outputTokens: 487,
365+
cacheReadTokens: 0,
366+
cacheWriteTokens: 0,
367+
cost: 0.0142215,
368+
userId: 'test-user-id',
369+
orgId: 'test-org-id',
370+
});
371+
372+
expect(llmEventData[2]).toMatchObject({
373+
taskId: 'test-task-from-file',
374+
type: 'LLM Completion',
375+
mode: 'code',
376+
inputTokens: 14399,
377+
outputTokens: 466,
378+
cacheReadTokens: 0,
379+
cacheWriteTokens: 0,
380+
cost: 0.01344465,
381+
userId: 'test-user-id',
382+
orgId: 'test-org-id',
383+
});
384+
385+
llmEventData.forEach((event) => {
386+
expect(typeof event.timestamp).toBe('number');
387+
});
388+
389+
const taskCompletedEventsResults = await analytics.query({
390+
query: `
391+
SELECT
392+
taskId,
393+
type,
394+
mode,
395+
userId,
396+
orgId,
397+
timestamp
398+
FROM events
399+
WHERE taskId = 'test-task-from-file' AND type = 'Task Completed'
400+
ORDER BY timestamp ASC
401+
`,
402+
format: 'JSONEachRow',
403+
});
404+
405+
const taskCompletedEventData =
406+
(await taskCompletedEventsResults.json()) as Array<{
407+
taskId: string;
408+
type: string;
409+
mode: string;
410+
userId: string;
411+
orgId: string;
412+
timestamp: number;
413+
}>;
414+
415+
expect(taskCompletedEventData).toHaveLength(2);
416+
417+
expect(taskCompletedEventData[0]).toMatchObject({
418+
taskId: 'test-task-from-file',
419+
type: 'Task Completed',
420+
mode: 'code',
421+
userId: 'test-user-id',
422+
orgId: 'test-org-id',
423+
});
424+
425+
expect(taskCompletedEventData[1]).toMatchObject({
426+
taskId: 'test-task-from-file',
427+
type: 'Task Completed',
428+
mode: 'code',
429+
userId: 'test-user-id',
430+
orgId: 'test-org-id',
431+
});
432+
433+
taskCompletedEventData.forEach((event) => {
434+
expect(typeof event.timestamp).toBe('number');
435+
});
304436
});
305437

306438
it('should return 401 if authentication fails', async () => {

apps/web/src/app/api/events/backfill/route.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,54 @@ export async function POST(request: NextRequest) {
109109
const timestamp = Math.round(message.ts / 1000);
110110
const mode = extractMode(message.text) || defaultMode;
111111

112+
if (message.say === 'api_req_started') {
113+
try {
114+
const result = apiReqStartedSchema.safeParse(
115+
JSON.parse(message.text || '{}'),
116+
);
117+
118+
if (
119+
result.success &&
120+
(result.data.tokensIn ||
121+
result.data.tokensOut ||
122+
result.data.cost)
123+
) {
124+
await captureEvent({
125+
id: uuidv4(),
126+
orgId,
127+
userId,
128+
timestamp,
129+
event: {
130+
type: TelemetryEventName.LLM_COMPLETION,
131+
properties: {
132+
taskId,
133+
...properties,
134+
mode,
135+
inputTokens: result.data.tokensIn ?? 0,
136+
outputTokens: result.data.tokensOut ?? 0,
137+
cacheReadTokens: result.data.cacheReads,
138+
cacheWriteTokens: result.data.cacheWrites,
139+
cost: result.data.cost,
140+
},
141+
},
142+
});
143+
}
144+
} catch {
145+
// Ignore JSON parsing and validation errors.
146+
}
147+
} else if (message.say === 'completion_result') {
148+
await captureEvent({
149+
id: uuidv4(),
150+
orgId,
151+
userId,
152+
timestamp,
153+
event: {
154+
type: TelemetryEventName.TASK_COMPLETED,
155+
properties: { taskId, ...properties, mode },
156+
},
157+
});
158+
}
159+
112160
const event = {
113161
type: TelemetryEventName.TASK_MESSAGE as const,
114162
properties: { taskId, message, ...properties, mode },
@@ -133,6 +181,14 @@ export async function POST(request: NextRequest) {
133181
}
134182
}
135183

184+
const apiReqStartedSchema = z.object({
185+
tokensIn: z.number().optional(),
186+
tokensOut: z.number().optional(),
187+
cacheReads: z.number().optional(),
188+
cacheWrites: z.number().optional(),
189+
cost: z.number().optional(),
190+
});
191+
136192
/**
137193
* Extracts the mode from a message text if it contains a <slug>mode</slug> pattern.
138194
* @param text - The message text to parse

0 commit comments

Comments
 (0)