Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 31 additions & 108 deletions app/api/stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,158 +2,81 @@ import { getRedisClient } from '@/lib/redis/config'
import { getModelUsageStats } from '@/lib/utils/rate-limit'
import { NextResponse } from 'next/server'

export const revalidate = 60 // Revalidate every 60 seconds

export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url)
const range = searchParams.get('range') || '24h'

const redis = await getRedisClient()
if (!redis) {
return NextResponse.json(
{ error: 'Redis not available' },
{ status: 500 }
)
return NextResponse.json({ error: 'Redis not available' }, { status: 500 })
}

const now = Date.now()
let startTime: number
const now = new Date()
let hours: number

switch (range) {
case '7d':
startTime = now - (7 * 24 * 60 * 60 * 1000)
hours = 7 * 24
break
case '30d':
startTime = now - (30 * 24 * 60 * 60 * 1000)
hours = 30 * 24
break
default: // 24h
startTime = now - (24 * 60 * 60 * 1000)
hours = 24
break
}

// Get real user statistics from Redis
const userIds = new Set<string>()
const authenticatedUsers = new Set<string>()
const anonymousUsers = new Set<string>()

// Get all rate limit keys to extract user information
try {
const allKeys = await redis.keys('rate_limit:*')

for (const key of allKeys) {
const parts = key.split(':')
if (parts.length >= 3) {
if (parts[1] === 'anonymous') {
const userId = parts[2]
anonymousUsers.add(userId)
userIds.add(userId)
} else {
const userId = parts[1]
authenticatedUsers.add(userId)
userIds.add(userId)
}
}
}
} catch (error) {
console.warn('Could not get user data from Redis:', error)
}
// Get user statistics
const authenticatedUsers = await redis.scard('stats:users:authenticated')
const anonymousUsers = await redis.scard('stats:users:anonymous')
const totalUsers = authenticatedUsers + anonymousUsers

// Get real hourly request data
// Get hourly request data
const hourlyRequests: Array<{ hour: string; count: number }> = []
const hourlyData: { [key: string]: number } = {}

// Generate hourly buckets
for (let i = hours - 1; i >= 0; i--) {
const hourTime = now - (i * 60 * 60 * 1000)
const hourKey = new Date(hourTime).toISOString().slice(0, 13) + ':00:00.000Z'
hourlyData[hourKey] = 0
}

// Count real requests from Redis
try {
const allKeys = await redis.keys('rate_limit:*')
for (const key of allKeys) {
const requests = await redis.zrange(key, 0, -1)
for (const req of requests) {
const reqTime = parseInt(req)
if (reqTime >= startTime) {
const hourKey = new Date(reqTime).toISOString().slice(0, 13) + ':00:00.000Z'
if (hourlyData[hourKey] !== undefined) {
hourlyData[hourKey]++
}
}
}
}
} catch (error) {
console.warn('Could not get hourly data from Redis:', error)
}

// Convert to array format
for (const [hour, count] of Object.entries(hourlyData)) {
hourlyRequests.push({ hour, count })
const hourlyData = (await redis.hgetall('stats:requests:hourly')) || {}
for (let i = 0; i < hours; i++) {
const hourTime = new Date(now.getTime() - i * 60 * 60 * 1000)
const hourKey = hourTime.toISOString().slice(0, 13) // YYYY-MM-DDTHH
hourlyRequests.push({
hour: `${hourKey}:00:00.000Z`, // The frontend expects this format
count: parseInt(hourlyData[hourKey] || '0'),
})
}

// Sort by time in descending order (newest first)
hourlyRequests.sort((a, b) => new Date(b.hour).getTime() - new Date(a.hour).getTime())

// Get real daily request data

// Get daily request data
const dailyRequests: Array<{ date: string; count: number }> = []
const dailyData: { [key: string]: number } = {}

const dailyData = (await redis.hgetall('stats:requests:daily')) || {}
const days = range === '30d' ? 30 : range === '7d' ? 7 : 1

// Generate daily buckets
for (let i = days - 1; i >= 0; i--) {
const dayTime = now - (i * 24 * 60 * 60 * 1000)
const dayKey = new Date(dayTime).toISOString().slice(0, 10)
dailyData[dayKey] = 0
}

// Count real daily requests from Redis
try {
const allKeys = await redis.keys('rate_limit:*')
for (const key of allKeys) {
const requests = await redis.zrange(key, 0, -1)
for (const req of requests) {
const reqTime = parseInt(req)
if (reqTime >= startTime) {
const dayKey = new Date(reqTime).toISOString().slice(0, 10)
if (dailyData[dayKey] !== undefined) {
dailyData[dayKey]++
}
}
}
}
} catch (error) {
console.warn('Could not get daily data from Redis:', error)
for (let i = 0; i < days; i++) {
const dayTime = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
const dayKey = dayTime.toISOString().slice(0, 10)
dailyRequests.push({
date: dayKey,
count: parseInt(dailyData[dayKey] || '0'),
})
}

// Convert to array format
for (const [date, count] of Object.entries(dailyData)) {
dailyRequests.push({ date, count })
}

// Sort by date in descending order (newest first)
dailyRequests.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())

// Get real model usage statistics from Redis
// Get model usage statistics
const topModels = await getModelUsageStats()

// Calculate total requests
const totalRequests = hourlyRequests.reduce((sum, hour) => sum + hour.count, 0)

const stats = {
totalUsers: userIds.size,
totalUsers,
totalRequests,
hourlyRequests,
dailyRequests,
topModels,
userTypes: {
authenticated: authenticatedUsers.size,
anonymous: anonymousUsers.size
authenticated: authenticatedUsers,
anonymous: anonymousUsers
}
}

Expand Down
5 changes: 0 additions & 5 deletions app/stats/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,6 @@ export default function StatsPage() {
fetchStats()
}, [fetchStats])

// Auto-refresh every 30 seconds
useEffect(() => {
const interval = setInterval(fetchStats, 30000)
return () => clearInterval(interval)
}, [fetchStats])

const formatTime = (timeString: string) => {
const date = new Date(timeString)
Expand Down
66 changes: 35 additions & 31 deletions lib/utils/rate-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export async function checkRateLimit(
// Add current request to both sorted sets
await redis.zadd(hourlyKey, now, now.toString())
await redis.zadd(quotaKey, now, now.toString())
await trackRequest(redis, userId, isAuthenticated)

// Remove old entries outside the window (simplified approach)
const hourlyRequestsToRemove = hourlyRequests.filter(req => {
Expand Down Expand Up @@ -220,6 +221,7 @@ export async function checkRateLimit(

// Add current request to the sorted set
await redis.zadd(key, now, now.toString())
await trackRequest(redis, userId, isAuthenticated)

// Remove old entries outside the window
const requestsToRemove = requests.filter(req => {
Expand All @@ -243,6 +245,27 @@ export async function checkRateLimit(
}
}

async function trackRequest(
redis: RedisWrapper,
userId: string,
isAuthenticated: boolean
) {
try {
const now = new Date()
const hour_key = now.toISOString().slice(0, 13)
const day_key = now.toISOString().slice(0, 10)
const userType = isAuthenticated ? 'authenticated' : 'anonymous'

const pipeline = redis.multi()
pipeline.hincrby('stats:requests:hourly', hour_key, 1)
pipeline.hincrby('stats:requests:daily', day_key, 1)
pipeline.sadd(`stats:users:${userType}`, userId)
await pipeline.exec()
} catch (error) {
console.error('Failed to track request:', error)
}
}

// Get user's current usage statistics
export async function getUserUsageStats(
userId: string,
Expand Down Expand Up @@ -344,56 +367,37 @@ export async function resetUserRateLimit(userId: string, modelId?: string): Prom
export async function trackModelUsage(modelId: string): Promise<void> {
const redis = await initializeRedisClient()
if (!redis) {
console.log('❌ Redis not available for model usage tracking')
return
}

const now = Date.now()
const modelUsageKey = `model_usage:${modelId}`

try {
// Add current usage to the model's sorted set
await redis.zadd(modelUsageKey, now, now.toString())

// Keep only the last 30 days of data (cleanup old entries)
const thirtyDaysAgo = now - (30 * 24 * 60 * 60 * 1000)
const oldEntries = await redis.zrangebyscore(modelUsageKey, 0, thirtyDaysAgo)
for (const entry of oldEntries) {
await redis.zrem(modelUsageKey, entry)
}

console.log(`✅ Tracked model usage for ${modelId}`)
await redis.zincrby('stats:models', 1, modelId)
} catch (error) {
console.error('Error tracking model usage:', error)
console.error('Error tracking model usage:', error)
}
}

// Get model usage statistics
export async function getModelUsageStats(): Promise<Array<{ model: string; count: number }>> {
const redis = await initializeRedisClient()
if (!redis) {
console.log('❌ Redis not available for model usage stats')
return []
}

try {
const modelKeys = await redis.keys('model_usage:*')
const modelStats: Array<{ model: string; count: number }> = []
// Get top 50 models by usage
const modelStats = await redis.zrevrange('stats:models', 0, 49, 'WITHSCORES')

for (const key of modelKeys) {
const modelId = key.replace('model_usage:', '')
const count = await redis.zcard(key)
if (count > 0) {
modelStats.push({ model: modelId, count })
}
const result: Array<{ model: string; count: number }> = []
for (let i = 0; i < modelStats.length; i += 2) {
result.push({
model: modelStats[i],
count: parseInt(modelStats[i + 1])
})
}

// Sort by count in descending order
modelStats.sort((a, b) => b.count - a.count)

return modelStats
return result
} catch (error) {
console.error('Error getting model usage stats:', error)
console.error('Error getting model usage stats:', error)
return []
}
}