diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index 4e25c1a4fa..b77152024c 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -2,7 +2,7 @@ import { auth } from '@clerk/nextjs/server'; import { NextRequest, NextResponse } from 'next/server'; import { v4 as uuidv4 } from 'uuid'; -import { eventSchema } from '@/schemas'; +import { cloudEventSchema } from '@/schemas'; import { captureEvent } from '@/lib/server/analytics'; export async function POST(request: NextRequest) { @@ -15,10 +15,9 @@ export async function POST(request: NextRequest) { ); } - const payload = await request.json(); const id = uuidv4(); - const timestamp = Date.now() / 1000; - const result = eventSchema.safeParse({ ...payload, id, userId, timestamp }); + const timestamp = Math.round(Date.now() / 1000); + const result = cloudEventSchema.safeParse(await request.json()); if (!result.success) { return NextResponse.json( @@ -28,8 +27,10 @@ export async function POST(request: NextRequest) { } try { - await captureEvent(result.data); + await captureEvent({ id, userId, timestamp, event: result.data }); } catch (error) { + console.error(error); + return NextResponse.json( { success: false, diff --git a/src/db/clickhouse.ts b/src/db/clickhouse.ts index 4ff4d0bae4..bb7759a675 100644 --- a/src/db/clickhouse.ts +++ b/src/db/clickhouse.ts @@ -1,22 +1,37 @@ -export const createEvents = ` - CREATE TABLE default.events - ( - \`id\` UUID, - \`userId\` String, - \`type\` String, - \`timestamp\` Int32, - \`taskId\` Nullable(String), - \`provider\` Nullable(String), - \`modelId\` Nullable(String), - \`prompt\` Nullable(String), - \`mode\` Nullable(String), - \`inputTokens\` Nullable(Int32), - \`outputTokens\` Nullable(Int32), - \`cacheReadTokens\` Nullable(Int32), - \`cacheWriteTokens\` Nullable(Int32), - \`cost\` Nullable(Float32) - ) - ENGINE = SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}') - ORDER BY (id, type, timestamp) - SETTINGS index_granularity = 8192; -`; +/* + +CREATE TABLE default.events +( + -- Shared + `id` UUID, + `userId` String, + `timestamp` Int32, + `type` String, + + -- App + `appVersion` String, + `vscodeVersion` String, + `platform` String, + `editorName` String, + `language` String, + `mode` String, + + -- Task + `taskId` Nullable(String), + `apiProvider` Nullable(String), + `modelId` Nullable(String), + `diffStrategy` Nullable(String), + `isSubtask` Nullable(Bool), + + -- Completion + `inputTokens` Nullable(Int32), + `outputTokens` Nullable(Int32), + `cacheReadTokens` Nullable(Int32), + `cacheWriteTokens` Nullable(Int32), + `cost` Nullable(Float32) +) +ENGINE = SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}') +ORDER BY (id, type, timestamp) +SETTINGS index_granularity = 8192; + +*/ diff --git a/src/lib/server/analytics.ts b/src/lib/server/analytics.ts index 8c7c4e9dc5..90274b29bb 100644 --- a/src/lib/server/analytics.ts +++ b/src/lib/server/analytics.ts @@ -1,6 +1,6 @@ import { createClient } from '@clickhouse/client'; -import { Event } from '@/schemas'; +import { CloudEvent } from '@/schemas'; import { Env } from './env'; const client = createClient({ @@ -9,10 +9,25 @@ const client = createClient({ password: Env.CLICKHOUSE_PASSWORD, }); -export const captureEvent = async ({ properties, ...event }: Event) => { +type AnalyticsEvent = { + id: string; + userId: string; + timestamp: number; + event: CloudEvent; +}; + +export const captureEvent = async ({ + event: { properties, ...cloudEvent }, + ...analyticsEvent +}: AnalyticsEvent) => { + // The destructuring here flattens the `AnalyticsEvent` to match the ClickHouse + // schema. + const value = { ...analyticsEvent, ...cloudEvent, ...properties }; + console.log(`captureEvent`, value); + await client.insert({ table: 'events', - values: [{ ...event, ...properties }], + values: [value], format: 'JSONEachRow', }); }; diff --git a/src/middleware.ts b/src/middleware.ts index f42a360e78..6beb90019b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -12,7 +12,7 @@ export default clerkMiddleware( await auth.protect(); } }, - { debug: process.env.NODE_ENV !== "production" }, + { debug: false }, ); // Also exclude tunnelRoute used in Sentry from the matcher. diff --git a/src/schemas/index.ts b/src/schemas/index.ts index eff722889a..aaba35188c 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,5 +1,10 @@ import { z } from 'zod'; +// TODO: I'll add these types to our @roo-code/types NPM package so we +// don't need to manually copy them. + +// Copied from `src/schemas/index.ts` + export const providerNames = [ 'anthropic', 'glama', @@ -24,36 +29,56 @@ export const providerNames = [ 'litellm', ] as const; -const baseEventSchema = z.object({ - id: z.string().uuid(), - userId: z.string(), - timestamp: z.number(), +// Copied from `src/services/telemetry/types.ts` + +export const appPropertiesSchema = z.object({ + appVersion: z.string(), + vscodeVersion: z.string(), + platform: z.string(), + editorName: z.string(), + language: z.string(), + mode: z.string(), }); -export const eventSchema = z.discriminatedUnion('type', [ - baseEventSchema.extend({ - type: z.literal('task_created'), +export const taskPropertiesSchema = z.object({ + taskId: z.string(), + apiProvider: z.enum(providerNames).optional(), + modelId: z.string().optional(), + diffStrategy: z.string().optional(), + isSubtask: z.boolean().optional(), +}); + +export const completionPropertiesSchema = z.object({ + inputTokens: z.number(), + outputTokens: z.number(), + cacheReadTokens: z.number().optional(), + cacheWriteTokens: z.number().optional(), + cost: z.number().optional(), +}); + +// Copied from `src/services/cloud/types.ts`. + +export enum CloudEventType { + TaskCreated = 'task_created', + Completion = 'completion', +} + +export const cloudEventSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal(CloudEventType.TaskCreated), properties: z.object({ - taskId: z.string(), - provider: z.enum(providerNames), - modelId: z.string(), - prompt: z.string(), - mode: z.string(), + ...appPropertiesSchema.shape, + ...taskPropertiesSchema.shape, }), }), - baseEventSchema.extend({ - type: z.literal('completion'), + z.object({ + type: z.literal(CloudEventType.Completion), properties: z.object({ - taskId: z.string(), - provider: z.enum(providerNames), - modelId: z.string(), - inputTokens: z.number(), - outputTokens: z.number(), - cacheReadTokens: z.number().optional(), - cacheWriteTokens: z.number().optional(), - cost: z.number().optional(), + ...appPropertiesSchema.shape, + ...taskPropertiesSchema.shape, + ...completionPropertiesSchema.shape, }), }), ]); -export type Event = z.infer; +export type CloudEvent = z.infer;