feat(copilot): stats tracking (#1227)

* Add copilot stats table schema

* Move db to agent

* Lint

* Fix tests
This commit is contained in:
Siddharth Ganesan
2025-09-02 18:17:50 -07:00
committed by GitHub
parent 1a5d5ddffa
commit c2d668c3eb
12 changed files with 283 additions and 71 deletions

View File

@@ -224,6 +224,7 @@ describe('Copilot Chat API Route', () => {
stream: true,
streamToolCalls: true,
mode: 'agent',
messageId: 'mock-uuid-1234-5678',
depth: 0,
}),
})
@@ -286,6 +287,7 @@ describe('Copilot Chat API Route', () => {
stream: true,
streamToolCalls: true,
mode: 'agent',
messageId: 'mock-uuid-1234-5678',
depth: 0,
}),
})
@@ -337,6 +339,7 @@ describe('Copilot Chat API Route', () => {
stream: true,
streamToolCalls: true,
mode: 'agent',
messageId: 'mock-uuid-1234-5678',
depth: 0,
}),
})
@@ -425,6 +428,7 @@ describe('Copilot Chat API Route', () => {
stream: true,
streamToolCalls: true,
mode: 'ask',
messageId: 'mock-uuid-1234-5678',
depth: 0,
}),
})

View File

@@ -108,6 +108,8 @@ export async function POST(req: NextRequest) {
conversationId,
contexts,
} = ChatMessageSchema.parse(body)
// Ensure we have a consistent user message ID for this request
const userMessageIdToUse = userMessageId || crypto.randomUUID()
try {
logger.info(`[${tracker.requestId}] Received chat POST`, {
hasContexts: Array.isArray(contexts),
@@ -369,6 +371,7 @@ export async function POST(req: NextRequest) {
stream: stream,
streamToolCalls: true,
mode: mode,
messageId: userMessageIdToUse,
...(providerConfig ? { provider: providerConfig } : {}),
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}),
@@ -414,7 +417,7 @@ export async function POST(req: NextRequest) {
if (stream && simAgentResponse.body) {
// Create user message to save
const userMessage = {
id: userMessageId || crypto.randomUUID(), // Use frontend ID if provided
id: userMessageIdToUse, // Consistent ID used for request and persistence
role: 'user',
content: message,
timestamp: new Date().toISOString(),
@@ -810,7 +813,7 @@ export async function POST(req: NextRequest) {
// Save messages if we have a chat
if (currentChat && responseData.content) {
const userMessage = {
id: userMessageId || crypto.randomUUID(), // Use frontend ID if provided
id: userMessageIdToUse, // Consistent ID used for request and persistence
role: 'user',
content: message,
timestamp: new Date().toISOString(),

View File

@@ -0,0 +1,80 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
createInternalServerErrorResponse,
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/auth'
import { env } from '@/lib/env'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
const BodySchema = z
.object({
// Do NOT send id; messageId is the unique correlator
userId: z.string().optional(),
chatId: z.string().uuid().optional(),
messageId: z.string().optional(),
depth: z.number().int().nullable().optional(),
maxEnabled: z.boolean().nullable().optional(),
createdAt: z.union([z.string().datetime(), z.date()]).optional(),
diffCreated: z.boolean().nullable().optional(),
diffAccepted: z.boolean().nullable().optional(),
duration: z.number().int().nullable().optional(),
inputTokens: z.number().int().nullable().optional(),
outputTokens: z.number().int().nullable().optional(),
aborted: z.boolean().nullable().optional(),
})
.passthrough()
export async function POST(req: NextRequest) {
const tracker = createRequestTracker()
try {
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !userId) {
return createUnauthorizedResponse()
}
const json = await req.json().catch(() => ({}))
const parsed = BodySchema.safeParse(json)
if (!parsed.success) {
return createBadRequestResponse('Invalid request body for copilot stats')
}
const body = parsed.data as any
// Build outgoing payload for Sim Agent; do not include id
const payload: Record<string, any> = {
...body,
userId: body.userId || userId,
createdAt: body.createdAt || new Date().toISOString(),
}
payload.id = undefined
const agentRes = await fetch(`${SIM_AGENT_API_URL}/api/stats`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}),
},
body: JSON.stringify(payload),
})
// Prefer not to block clients; still relay status
let agentJson: any = null
try {
agentJson = await agentRes.json()
} catch {}
if (!agentRes.ok) {
const message = (agentJson && (agentJson.error || agentJson.message)) || 'Upstream error'
return NextResponse.json({ success: false, error: message }, { status: 400 })
}
return NextResponse.json({ success: true })
} catch (error) {
return createInternalServerErrorResponse('Failed to forward copilot stats')
}
}