Skip to content

Commit 075d2d4

Browse files
authored
Emit a TASK_CREATE event when backfilling (#128)
1 parent 2491819 commit 075d2d4

File tree

2 files changed

+231
-1
lines changed

2 files changed

+231
-1
lines changed

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

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ describe('/api/events/backfill', () => {
3737
await analytics.command({
3838
query: `DELETE FROM messages WHERE taskId LIKE 'test-%'`,
3939
});
40+
await analytics.command({
41+
query: `DELETE FROM events WHERE taskId LIKE 'test-%'`,
42+
});
4043
} catch {
4144
// Ignore cleanup errors.
4245
}
@@ -92,6 +95,41 @@ describe('/api/events/backfill', () => {
9295
expect(response.status).toBe(200);
9396
expect(responseData.success).toBe(true);
9497

98+
const eventsResults = await analytics.query({
99+
query: `
100+
SELECT
101+
taskId,
102+
type,
103+
mode,
104+
userId,
105+
orgId,
106+
timestamp
107+
FROM events
108+
WHERE taskId = 'test-task-integration' AND type = 'Task Created'
109+
ORDER BY timestamp ASC
110+
`,
111+
format: 'JSONEachRow',
112+
});
113+
114+
const eventData = (await eventsResults.json()) as Array<{
115+
taskId: string;
116+
type: string;
117+
mode: string;
118+
userId: string;
119+
orgId: string;
120+
timestamp: number;
121+
}>;
122+
123+
expect(eventData).toHaveLength(1);
124+
expect(eventData[0]).toMatchObject({
125+
taskId: 'test-task-integration',
126+
type: 'Task Created',
127+
mode: 'code',
128+
userId: 'test-user-id',
129+
orgId: 'test-org-id',
130+
});
131+
expect(typeof eventData[0]?.timestamp).toBe('number');
132+
95133
const dbResults = await analytics.query({
96134
query: `
97135
SELECT
@@ -175,6 +213,41 @@ describe('/api/events/backfill', () => {
175213
expect(response.status).toBe(200);
176214
expect(responseData.success).toBe(true);
177215

216+
const eventsResults = await analytics.query({
217+
query: `
218+
SELECT
219+
taskId,
220+
type,
221+
mode,
222+
userId,
223+
orgId,
224+
timestamp
225+
FROM events
226+
WHERE taskId = 'test-task-from-file' AND type = 'Task Created'
227+
ORDER BY timestamp ASC
228+
`,
229+
format: 'JSONEachRow',
230+
});
231+
232+
const eventData = (await eventsResults.json()) as Array<{
233+
taskId: string;
234+
type: string;
235+
mode: string;
236+
userId: string;
237+
orgId: string;
238+
timestamp: number;
239+
}>;
240+
241+
expect(eventData).toHaveLength(1);
242+
expect(eventData[0]).toMatchObject({
243+
taskId: 'test-task-from-file',
244+
type: 'Task Created',
245+
mode: 'code',
246+
userId: 'test-user-id',
247+
orgId: 'test-org-id',
248+
});
249+
expect(typeof eventData[0]?.timestamp).toBe('number');
250+
178251
const dbResults = await analytics.query({
179252
query: `
180253
SELECT
@@ -263,6 +336,14 @@ describe('/api/events/backfill', () => {
263336

264337
const dbData = (await dbResults.json()) as Array<{ count: string }>;
265338
expect(dbData[0]?.count).toBe('0');
339+
340+
const eventsResults = await analytics.query({
341+
query: `SELECT COUNT() as count FROM events WHERE taskId = 'test' AND type = 'Task Created'`,
342+
format: 'JSONEachRow',
343+
});
344+
345+
const eventsData = (await eventsResults.json()) as Array<{ count: string }>;
346+
expect(eventsData[0]?.count).toBe('0');
266347
});
267348

268349
it('should return 400 if no file is provided', async () => {
@@ -440,6 +521,41 @@ describe('/api/events/backfill', () => {
440521
expect(response.status).toBe(200);
441522
expect(responseData.success).toBe(true);
442523

524+
const eventsResults = await analytics.query({
525+
query: `
526+
SELECT
527+
taskId,
528+
type,
529+
mode,
530+
userId,
531+
orgId,
532+
timestamp
533+
FROM events
534+
WHERE taskId = 'test-task-mode-extraction' AND type = 'Task Created'
535+
ORDER BY timestamp ASC
536+
`,
537+
format: 'JSONEachRow',
538+
});
539+
540+
const eventData = (await eventsResults.json()) as Array<{
541+
taskId: string;
542+
type: string;
543+
mode: string;
544+
userId: string;
545+
orgId: string;
546+
timestamp: number;
547+
}>;
548+
549+
expect(eventData).toHaveLength(1);
550+
expect(eventData[0]).toMatchObject({
551+
taskId: 'test-task-mode-extraction',
552+
type: 'Task Created',
553+
mode: 'debug',
554+
userId: 'test-user-id',
555+
orgId: 'test-org-id',
556+
});
557+
expect(typeof eventData[0]?.timestamp).toBe('number');
558+
443559
const dbResults = await analytics.query({
444560
query: `
445561
SELECT
@@ -490,6 +606,107 @@ describe('/api/events/backfill', () => {
490606
});
491607
});
492608

609+
it('should emit TASK_CREATED event with correct properties and timestamp', async () => {
610+
mockAuthorizeApi.mockResolvedValue({
611+
success: true,
612+
userId: 'test-user-id',
613+
orgId: 'test-org-id',
614+
userType: 'user',
615+
orgRole: 'admin',
616+
} as unknown as ApiAuthResult);
617+
618+
const messages = [
619+
{
620+
ts: 1750702747687,
621+
type: 'say' as const,
622+
say: 'text' as const,
623+
text: 'Test task creation',
624+
images: [],
625+
},
626+
];
627+
628+
const fileContent = JSON.stringify(messages);
629+
const file = new File([fileContent], 'test-messages.json', {
630+
type: 'application/json',
631+
});
632+
633+
const customProperties = {
634+
...testProperties,
635+
mode: 'architect',
636+
apiProvider: 'openai',
637+
modelId: 'gpt-4',
638+
};
639+
640+
const formData = new FormData();
641+
formData.append('file', file);
642+
formData.append('taskId', 'test-task-created-event');
643+
formData.append('properties', JSON.stringify(customProperties));
644+
645+
const request = new NextRequest(
646+
'http://localhost:3000/api/events/backfill',
647+
{
648+
method: 'POST',
649+
body: formData,
650+
},
651+
);
652+
653+
const response = await POST(request);
654+
const responseData = await response.json();
655+
656+
expect(response.status).toBe(200);
657+
expect(responseData.success).toBe(true);
658+
659+
const eventsResults = await analytics.query({
660+
query: `
661+
SELECT
662+
taskId,
663+
type,
664+
mode,
665+
userId,
666+
orgId,
667+
timestamp,
668+
apiProvider,
669+
modelId,
670+
appVersion,
671+
platform
672+
FROM events
673+
WHERE taskId = 'test-task-created-event' AND type = 'Task Created'
674+
`,
675+
format: 'JSONEachRow',
676+
});
677+
678+
const eventData = (await eventsResults.json()) as Array<{
679+
taskId: string;
680+
type: string;
681+
mode: string;
682+
userId: string;
683+
orgId: string;
684+
timestamp: number;
685+
apiProvider: string;
686+
modelId: string;
687+
appVersion: string;
688+
platform: string;
689+
}>;
690+
691+
expect(eventData).toHaveLength(1);
692+
693+
const taskCreatedEvent = eventData[0]!;
694+
expect(taskCreatedEvent).toMatchObject({
695+
taskId: 'test-task-created-event',
696+
type: 'Task Created',
697+
mode: 'architect',
698+
userId: 'test-user-id',
699+
orgId: 'test-org-id',
700+
apiProvider: 'openai',
701+
modelId: 'gpt-4',
702+
appVersion: '1.0.0',
703+
platform: 'darwin',
704+
});
705+
706+
const expectedTimestamp = Math.round(messages[0]!.ts / 1000);
707+
expect(taskCreatedEvent.timestamp).toBe(expectedTimestamp);
708+
});
709+
493710
it('should handle invalid ClineMessage schema', async () => {
494711
mockAuthorizeApi.mockResolvedValue({
495712
success: true,

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ export async function POST(request: NextRequest) {
8080
);
8181
}
8282

83-
if (messages.length === 0) {
83+
const message = messages[0];
84+
85+
if (!message) {
8486
return NextResponse.json(
8587
{ success: false, error: 'File contains no messages' },
8688
{ status: 400 },
@@ -89,6 +91,17 @@ export async function POST(request: NextRequest) {
8991

9092
const defaultMode = extractMode(messages[0]?.text) || properties.mode;
9193

94+
await captureEvent({
95+
id: uuidv4(),
96+
orgId,
97+
userId,
98+
timestamp: Math.round(message.ts / 1000),
99+
event: {
100+
type: TelemetryEventName.TASK_CREATED,
101+
properties: { taskId, ...properties, mode: defaultMode },
102+
},
103+
});
104+
92105
await pMap(
93106
messages,
94107
async (message) => {

0 commit comments

Comments
 (0)