Skip to content

Commit 95b5631

Browse files
committed
Add /api/agents/metrics endpoint for client-side metrics loading
- New endpoint returns metrics keyed by publisherId/agentName - Supports progressive loading pattern for store page - Metrics load client-side while basic agent info loads server-side
1 parent 8ea6091 commit 95b5631

File tree

6 files changed

+368
-46
lines changed

6 files changed

+368
-46
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NextResponse } from 'next/server'
2+
3+
import { logger } from '@/util/logger'
4+
import { applyCacheHeaders } from '@/server/apply-cache-headers'
5+
import { getCachedAgentsMetrics } from '@/server/agents-data'
6+
7+
// ISR Configuration for API route - metrics can be cached
8+
export const revalidate = 600 // Cache for 10 minutes
9+
export const dynamic = 'force-static'
10+
11+
export async function GET() {
12+
try {
13+
const metrics = await getCachedAgentsMetrics()
14+
15+
const response = NextResponse.json(metrics)
16+
return applyCacheHeaders(response)
17+
} catch (error) {
18+
logger.error({ error }, 'Error fetching agent metrics')
19+
return NextResponse.json(
20+
{ error: 'Internal server error' },
21+
{ status: 500 },
22+
)
23+
}
24+
}

web/src/app/store/page.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Metadata } from 'next'
22
import { env } from '@codebuff/common/env'
3-
import { getCachedAgentsLite } from '@/server/agents-data'
3+
import { getCachedAgentsBasicInfo } from '@/server/agents-data'
44
import AgentStoreClient from './store-client'
55

66
interface PublisherProfileResponse {
@@ -16,7 +16,7 @@ export async function generateMetadata(): Promise<Metadata> {
1616
publisher?: { avatar_url?: string | null }
1717
}> = []
1818
try {
19-
agents = await getCachedAgentsLite()
19+
agents = await getCachedAgentsBasicInfo()
2020
} catch (error) {
2121
console.error('[Store] Failed to fetch agents for metadata:', error)
2222
agents = []
@@ -92,10 +92,11 @@ function StoreJsonLd({ agentCount }: { agentCount: number }) {
9292

9393
export default async function StorePage({ searchParams }: StorePageProps) {
9494
const resolvedSearchParams = await searchParams
95-
// Fetch agents data on the server with ISR cache
95+
// Fetch only basic agent info on the server - metrics load client-side
96+
// This keeps the initial payload small and cacheable
9697
let agentsData: any[] = []
9798
try {
98-
agentsData = await getCachedAgentsLite()
99+
agentsData = await getCachedAgentsBasicInfo()
99100
} catch (error) {
100101
console.error('[Store] Failed to fetch agents data:', error)
101102
agentsData = []

web/src/app/store/store-client.tsx

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ import { cn } from '@/lib/utils'
3535
import type { Session } from 'next-auth'
3636
import { create } from 'zustand'
3737

38-
interface AgentData {
38+
// Basic agent info from SSR (no metrics)
39+
interface AgentBasicInfo {
3940
id: string
4041
name: string
4142
description?: string
@@ -47,15 +48,29 @@ interface AgentData {
4748
}
4849
version: string
4950
created_at: string
51+
tags?: string[]
52+
}
53+
54+
// Metrics loaded client-side
55+
interface AgentMetrics {
56+
usage_count: number
57+
weekly_runs: number
58+
weekly_spent: number
59+
total_spent: number
60+
avg_cost_per_invocation: number
61+
unique_users: number
62+
last_used?: string
63+
}
64+
65+
// Combined data for display
66+
interface AgentData extends AgentBasicInfo {
5067
usage_count?: number
5168
weekly_runs?: number
5269
weekly_spent?: number
5370
total_spent?: number
5471
avg_cost_per_invocation?: number
5572
unique_users?: number
5673
last_used?: string
57-
version_stats?: Record<string, any>
58-
tags?: string[]
5974
}
6075

6176
interface AgentStoreState {
@@ -92,7 +107,7 @@ interface PublisherProfileResponse {
92107
}
93108

94109
interface AgentStoreClientProps {
95-
initialAgents: AgentData[]
110+
initialAgents: AgentBasicInfo[]
96111
initialPublishers: PublisherProfileResponse[]
97112
session: Session | null
98113
searchParams: { [key: string]: string | string[] | undefined }
@@ -199,24 +214,42 @@ export default function AgentStoreClient({
199214
loadingStateRef.current = { isLoadingMore, hasMore }
200215
}, [isLoadingMore, hasMore])
201216

202-
// Hydrate agents client-side if SSR provided none (build-time fallback)
203-
const { data: hydratedAgents } = useQuery<AgentData[]>({
204-
queryKey: ['agents'],
217+
// Fetch metrics client-side - this is the progressive loading part
218+
const { data: metricsMap, isLoading: isLoadingMetrics } = useQuery<
219+
Record<string, AgentMetrics>
220+
>({
221+
queryKey: ['agents-metrics'],
205222
queryFn: async () => {
206-
const response = await fetch('/api/agents')
223+
const response = await fetch('/api/agents/metrics')
207224
if (!response.ok) {
208-
throw new Error(`Failed to fetch agents: ${response.statusText}`)
225+
throw new Error(`Failed to fetch metrics: ${response.statusText}`)
209226
}
210227
return response.json()
211228
},
212-
enabled: (initialAgents?.length ?? 0) === 0,
213229
staleTime: 600000, // 10 minutes
214230
})
215231

216-
// Prefer hydrated data if present; else use SSR data
217-
const agents = useMemo(() => {
218-
return hydratedAgents ?? initialAgents
219-
}, [hydratedAgents, initialAgents])
232+
// Combine basic agent info with metrics when available
233+
const agents: AgentData[] = useMemo(() => {
234+
if (!initialAgents?.length) return []
235+
236+
return initialAgents.map((agent) => {
237+
// Key matches how metrics are stored: publisherId/agentName
238+
const metricsKey = `${agent.publisher.id}/${agent.name}`
239+
const metrics = metricsMap?.[metricsKey]
240+
241+
return {
242+
...agent,
243+
usage_count: metrics?.usage_count,
244+
weekly_runs: metrics?.weekly_runs,
245+
weekly_spent: metrics?.weekly_spent,
246+
total_spent: metrics?.total_spent,
247+
avg_cost_per_invocation: metrics?.avg_cost_per_invocation,
248+
unique_users: metrics?.unique_users,
249+
last_used: metrics?.last_used,
250+
}
251+
})
252+
}, [initialAgents, metricsMap])
220253

221254
const editorsChoice = useMemo(() => {
222255
return agents.filter((agent) => EDITORS_CHOICE_AGENTS.includes(agent.id))
@@ -503,55 +536,75 @@ export default function AgentStoreClient({
503536
</Badge>
504537
)}
505538
</div>
506-
{agent.last_used && (
507-
<span
508-
className="text-xs text-muted-foreground/60"
509-
title={new Date(agent.last_used).toLocaleString()}
510-
>
511-
Used <RelativeTime date={agent.last_used} />
512-
</span>
539+
{isLoadingMetrics ? (
540+
<div className="h-4 w-20 bg-muted/30 rounded animate-pulse" />
541+
) : (
542+
agent.last_used && (
543+
<span
544+
className="text-xs text-muted-foreground/60"
545+
title={new Date(agent.last_used).toLocaleString()}
546+
>
547+
Used <RelativeTime date={agent.last_used} />
548+
</span>
549+
)
513550
)}
514551
</div>
515552
</div>
516553

517-
{/* Metrics Grid - Redesigned for better readability */}
554+
{/* Metrics Grid - Shows skeleton while loading */}
518555
<div className="grid grid-cols-2 gap-3 py-3 border-t border-border/30">
519556
<div className="space-y-1">
520557
<div className="flex items-center gap-2">
521558
<TrendingUp className="h-4 w-4 text-emerald-400" />
522-
<span className="font-semibold text-emerald-400">
523-
{formatCurrency(agent.weekly_spent)}
524-
</span>
559+
{isLoadingMetrics ? (
560+
<div className="h-5 w-12 bg-muted/50 rounded animate-pulse" />
561+
) : (
562+
<span className="font-semibold text-emerald-400">
563+
{formatCurrency(agent.weekly_spent)}
564+
</span>
565+
)}
525566
</div>
526567
<p className="text-xs text-muted-foreground">Weekly spend</p>
527568
</div>
528569

529570
<div className="space-y-1">
530571
<div className="flex items-center gap-2">
531572
<Play className="h-4 w-4 text-muted-foreground" />
532-
<span className="font-semibold">
533-
{formatUsageCount(agent.weekly_runs)}
534-
</span>
573+
{isLoadingMetrics ? (
574+
<div className="h-5 w-10 bg-muted/50 rounded animate-pulse" />
575+
) : (
576+
<span className="font-semibold">
577+
{formatUsageCount(agent.weekly_runs)}
578+
</span>
579+
)}
535580
</div>
536581
<p className="text-xs text-muted-foreground">Weekly runs</p>
537582
</div>
538583

539584
<div className="space-y-1">
540585
<div className="flex items-center gap-2">
541586
<DollarSign className="h-4 w-4 text-muted-foreground" />
542-
<span className="font-semibold">
543-
{formatCurrency(agent.avg_cost_per_invocation)}
544-
</span>
587+
{isLoadingMetrics ? (
588+
<div className="h-5 w-12 bg-muted/50 rounded animate-pulse" />
589+
) : (
590+
<span className="font-semibold">
591+
{formatCurrency(agent.avg_cost_per_invocation)}
592+
</span>
593+
)}
545594
</div>
546595
<p className="text-xs text-muted-foreground">Per run</p>
547596
</div>
548597

549598
<div className="space-y-1">
550599
<div className="flex items-center gap-2">
551600
<Users className="h-4 w-4 text-muted-foreground" />
552-
<span className="font-semibold">
553-
{agent.unique_users || 0}
554-
</span>
601+
{isLoadingMetrics ? (
602+
<div className="h-5 w-8 bg-muted/50 rounded animate-pulse" />
603+
) : (
604+
<span className="font-semibold">
605+
{agent.unique_users || 0}
606+
</span>
607+
)}
555608
</div>
556609
<p className="text-xs text-muted-foreground">Users</p>
557610
</div>

web/src/server/__tests__/agents-transform.test.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
buildAgentsData,
44
buildAgentsDataLite,
55
type AgentRow,
6+
type AgentRowSlim,
67
} from '../agents-transform'
78

89
describe('buildAgentsData', () => {
@@ -262,11 +263,14 @@ describe('buildAgentsData', () => {
262263

263264
describe('buildAgentsDataLite', () => {
264265
it('dedupes by latest, merges metrics, and omits version_stats', () => {
265-
const agents: AgentRow[] = [
266+
// AgentRowSlim has pre-extracted fields (name, description, tags) instead of data blob
267+
const agents: AgentRowSlim[] = [
266268
{
267269
id: 'base',
268270
version: '1.0.0',
269-
data: { name: 'Base', description: 'desc', tags: ['x'] },
271+
name: 'Base',
272+
description: 'desc',
273+
tags: ['x'],
270274
created_at: '2025-01-01T00:00:00.000Z',
271275
publisher: {
272276
id: 'codebuff',
@@ -279,7 +283,9 @@ describe('buildAgentsDataLite', () => {
279283
{
280284
id: 'base-old',
281285
version: '0.9.0',
282-
data: { name: 'Base', description: 'old' },
286+
name: 'Base',
287+
description: 'old',
288+
tags: null,
283289
created_at: '2024-12-01T00:00:00.000Z',
284290
publisher: {
285291
id: 'codebuff',
@@ -291,7 +297,9 @@ describe('buildAgentsDataLite', () => {
291297
{
292298
id: 'reviewer',
293299
version: '2.1.0',
294-
data: { name: 'Reviewer' },
300+
name: 'Reviewer',
301+
description: null,
302+
tags: null,
295303
created_at: '2025-01-03T00:00:00.000Z',
296304
publisher: {
297305
id: 'codebuff',
@@ -365,11 +373,14 @@ describe('buildAgentsDataLite', () => {
365373
})
366374

367375
it('handles missing metrics gracefully and omits version_stats', () => {
368-
const agents = [
376+
// AgentRowSlim with null name (should fall back to id)
377+
const agents: AgentRowSlim[] = [
369378
{
370379
id: 'solo',
371380
version: '0.1.0',
372-
data: { description: 'no name provided' },
381+
name: null,
382+
description: 'no name provided',
383+
tags: null,
373384
created_at: new Date('2025-02-01T00:00:00.000Z'),
374385
publisher: {
375386
id: 'codebuff',
@@ -378,7 +389,7 @@ describe('buildAgentsDataLite', () => {
378389
avatar_url: null,
379390
},
380391
},
381-
] as any
392+
]
382393

383394
const out = buildAgentsDataLite({
384395
agents,

0 commit comments

Comments
 (0)