feat: enhance usage analytics dashboard (#666)

- visualize input, output, and cache token segments in usage charts
- add usage period selector for 7d, 30d, 90d, and 365d
- guard usage stats against stale overlapping period requests
- normalize blank model usage into unknown buckets
- add client and server coverage for usage analytics behavior
This commit is contained in:
Zhicheng Han
2026-05-13 01:41:49 +02:00
committed by GitHub
parent 57cdf87bef
commit c2068302c3
18 changed files with 683 additions and 113 deletions
+95 -12
View File
@@ -11,6 +11,10 @@ interface DailyUsage {
sessions: number
errors: number
cost: number
visualTokens: number
inputPercent: number
outputPercent: number
cachePercent: number
}
interface ModelUsage {
@@ -18,22 +22,70 @@ interface ModelUsage {
inputTokens: number
outputTokens: number
cacheTokens: number
cacheWriteTokens: number
totalTokens: number
visualTokens: number
sessions: number
color: string
inputPercent: number
outputPercent: number
cachePercent: number
}
const MODEL_COLORS = [
'#4fd1c5',
'#63b3ed',
'#f6ad55',
'#b794f4',
'#68d391',
'#fc8181',
'#f687b3',
'#90cdf4',
'#fbd38d',
'#9ae6b4',
]
function normalizeModel(model: string | null | undefined): string {
const trimmed = (model || '').trim()
return trimmed || 'unknown'
}
function percent(part: number, total: number): number {
if (total <= 0) return 0
return part / total * 100
}
function getModelColor(model: string): string {
const normalized = normalizeModel(model)
let hash = 0
for (let i = 0; i < normalized.length; i += 1) {
hash = ((hash << 5) - hash) + normalized.charCodeAt(i)
hash |= 0
}
return MODEL_COLORS[Math.abs(hash) % MODEL_COLORS.length]
}
export const useUsageStore = defineStore('usage', () => {
const stats = ref<UsageStatsResponse | null>(null)
const isLoading = ref(false)
let latestRequestId = 0
async function loadSessions(days = 30) {
const requestId = ++latestRequestId
isLoading.value = true
try {
stats.value = await fetchUsageStats(days)
const response = await fetchUsageStats(days)
if (requestId === latestRequestId) {
stats.value = response
}
} catch (err) {
console.error('Failed to load usage stats:', err)
if (requestId === latestRequestId) {
console.error('Failed to load usage stats:', err)
}
} finally {
isLoading.value = false
if (requestId === latestRequestId) {
isLoading.value = false
}
}
}
@@ -56,17 +108,46 @@ export const useUsageStore = defineStore('usage', () => {
const modelUsage = computed<ModelUsage[]>(() => {
if (!stats.value) return []
return stats.value.model_usage.map(m => ({
model: m.model || 'unknown',
inputTokens: m.input_tokens,
outputTokens: m.output_tokens,
cacheTokens: m.cache_read_tokens,
totalTokens: m.input_tokens + m.output_tokens,
sessions: m.sessions,
})).sort((a, b) => b.totalTokens - a.totalTokens)
return stats.value.model_usage.map(m => {
const model = normalizeModel(m.model)
const totalTokens = m.input_tokens + m.output_tokens
const visualTokens = totalTokens + m.cache_read_tokens
return {
model,
inputTokens: m.input_tokens,
outputTokens: m.output_tokens,
cacheTokens: m.cache_read_tokens,
cacheWriteTokens: m.cache_write_tokens,
totalTokens,
visualTokens,
sessions: m.sessions,
color: getModelColor(model),
inputPercent: percent(m.input_tokens, visualTokens),
outputPercent: percent(m.output_tokens, visualTokens),
cachePercent: percent(m.cache_read_tokens, visualTokens),
}
}).sort((a, b) => b.visualTokens - a.visualTokens)
})
const dailyUsage = computed<DailyUsage[]>(() => stats.value?.daily_usage ?? [])
const modelLegend = computed(() => {
const seen = new Set<string>()
return modelUsage.value.filter(m => {
if (seen.has(m.model)) return false
seen.add(m.model)
return true
}).map(m => ({ model: m.model, color: m.color }))
})
const dailyUsage = computed<DailyUsage[]>(() => (stats.value?.daily_usage ?? []).map(d => {
const visualTokens = d.input_tokens + d.output_tokens + d.cache_read_tokens
return {
...d,
visualTokens,
inputPercent: percent(d.input_tokens, visualTokens),
outputPercent: percent(d.output_tokens, visualTokens),
cachePercent: percent(d.cache_read_tokens, visualTokens),
}
}))
const avgSessionsPerDay = computed(() => {
if (!stats.value || stats.value.daily_usage.length === 0) return 0
@@ -88,7 +169,9 @@ export const useUsageStore = defineStore('usage', () => {
cacheHitRate,
estimatedCost,
modelUsage,
modelLegend,
dailyUsage,
avgSessionsPerDay,
getModelColor,
}
})