Skip to content

## Feature: Add API Usage Dashboard Showing Rate Limits and Quota Consumption #972

@Pcmhacker-piro

Description

@Pcmhacker-piro

Feature: Add API Usage Dashboard Showing Rate Limits and Quota Consumption


Description

AegisAI has per-user rate limiting on its API endpoints (Guard scan, RAG query, etc.) and stores rate limit data in Redis. However, there is no user-facing UI showing current usage, remaining quota, or rate limit history. Users hit 429 Too Many Requests errors with no way to monitor or understand their consumption patterns. This issue proposes adding an API Usage page that visualises request counts, rate limit hits, and remaining quota across all rate-limited endpoints, plus a per-endpoint breakdown.


Proposed Solution

Add a new GET /api/v1/analytics/usage endpoint that queries the Redis rate limiter for current counters (or a PostgreSQL api_usage_log table if persistent tracking is preferred). The frontend displays a dashboard with: a summary card showing remaining requests / total limit, a per-endpoint breakdown table with request count and limit, a time-series chart of requests over the last 7/30 days, and a list of recent 429 events. The Guard scan and RAG query endpoints get specific focus since they consume AI credits.


Acceptance Criteria

  • A new "API Usage" page is accessible from the sidebar navigation (between Analytics and Settings)
  • The page shows a summary card: "1,234 / 5,000 requests used today" with a progress bar colour-coded (green < 70%, yellow < 90%, red > 90%)
  • A per-endpoint breakdown table with columns: Endpoint, Requests Today, Limit, Remaining, Reset Time
  • A line chart (Recharts) showing daily request volume over the last 7 days (toggleable to 30 days)
  • A "Recent Rate Limit Events" section listing the last 10 times the user hit a 429, with timestamp and endpoint
  • The Guard scan endpoint is highlighted separately with its own card showing AI credit consumption
  • The RAG query endpoint shows a separate card with query count and token usage estimate
  • The data refreshes automatically every 60 seconds (or on page focus)
  • An empty state is shown if no data is available yet
  • Loading skeleton while data fetches
  • The page respects mobile layout (stacks vertically)

Requirements

  • Backend:
    • New endpoint: GET /api/v1/analytics/usage returns aggregated usage stats for the current user
    • Reads rate limit counters from Redis (or a new ApiUsageLog model for persistent storage)
    • Returns: { daily: { total_requests, limit, remaining, reset_at }, endpoints: [...], history: [...], recent_429s: [...] }
    • The endpoint itself should not be rate-limited or should count towards its own limit separately
    • If Redis is not available, return mock/zero data gracefully
  • Frontend:
    • ApiUsagePage component with cards, table, and chart
    • UsageSummaryCard — progress bar with count/limit
    • EndpointBreakdownTable — table of per-endpoint stats
    • UsageHistoryChart — Recharts line chart
    • RecentRateLimitEvents — list of 429 events
    • Auto-refresh with useInterval or TanStack Query's refetchInterval
    • Theme-aware colours for charts (dark mode support)

Implementation Hints

# backend/app/api/v1/analytics.py
import json
from datetime import datetime, timedelta

@app.get("/api/v1/analytics/usage")
async def get_api_usage(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
    redis_client: Redis = Depends(get_redis),
):
    user_id = current_user.id
    today = datetime.utcnow().date().isoformat()
    usage_key = f"usage:{user_id}:{today}"

    # Get daily counters from Redis
    daily_data = {}
    if redis_client:
        daily_data = redis_client.hgetall(usage_key) or {}

    # Per-endpoint breakdown
    endpoints = [
        {"name": "Guard Scan", "key": "guard_scan", "limit": 1000},
        {"name": "RAG Query", "key": "rag_query", "limit": 500},
        {"name": "AI System CRUD", "key": "ai_systems", "limit": 2000},
        {"name": "Document Operations", "key": "documents", "limit": 1000},
        {"name": "Classification", "key": "classification", "limit": 500},
    ]

    endpoint_stats = []
    total_requests = 0
    total_limit = 0

    for ep in endpoints:
        count = int(daily_data.get(ep["key"], 0))
        total_requests += count
        total_limit += ep["limit"]
        endpoint_stats.append({
            "endpoint": ep["name"],
            "requests": count,
            "limit": ep["limit"],
            "remaining": max(0, ep["limit"] - count),
            "reset_at": (datetime.utcnow() + timedelta(days=1)).replace(hour=0, minute=0, second=0).isoformat(),
        })

    # History (last 7 days from persistent log or Redis)
    history = []
    if redis_client:
        for i in range(7):
            day = (datetime.utcnow() - timedelta(days=i)).date().isoformat()
            day_key = f"usage:{user_id}:{day}"
            day_total = sum(
                int(v) for v in (redis_client.hgetall(day_key) or {}).values()
            )
            history.append({"date": day, "requests": day_total})
    history.reverse()

    # Recent 429 events
    recent_429s = []
    if redis_client:
        limit_events = redis_client.lrange(f"rate_limit_events:{user_id}", 0, 9) or []
        for event in limit_events:
            try:
                recent_429s.append(json.loads(event))
            except json.JSONDecodeError:
                continue

    return {
        "daily": {
            "total_requests": total_requests,
            "total_limit": total_limit,
            "remaining": max(0, total_limit - total_requests),
            "reset_at": (datetime.utcnow() + timedelta(days=1)).replace(hour=0, minute=0, second=0).isoformat(),
        },
        "endpoints": endpoint_stats,
        "history": history,
        "recent_429s": recent_429s,
        "guard_scan": {
            "requests": int(daily_data.get("guard_scan", 0)),
            "limit": 1000,
            "ai_credits_used": int(daily_data.get("guard_scan", 0)) * 0.5,  # estimated
        },
        "rag_query": {
            "requests": int(daily_data.get("rag_query", 0)),
            "limit": 500,
            "estimated_tokens": int(daily_data.get("rag_query", 0)) * 1500,
        },
    }
// frontend/src/pages/ApiUsagePage.tsx
'use client';

import { useQuery } from '@tanstack/react-query';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import { Activity, AlertTriangle, Shield, BookOpen } from 'lucide-react';
import { api } from '@/services/api';

export const ApiUsagePage = () => {
  const { data, isLoading } = useQuery({
    queryKey: ['api-usage'],
    queryFn: () => api.get('/api/v1/analytics/usage').then(r => r.data),
    refetchInterval: 60_000, // auto-refresh every 60s
  });

  if (isLoading) return <ApiUsageSkeleton />;

  const usagePercent = data ? (data.daily.total_requests / data.daily.total_limit) * 100 : 0;

  return (
    <div className="max-w-5xl mx-auto px-4 py-6 space-y-6">
      <h1 className="text-2xl font-bold flex items-center gap-2">
        <Activity className="w-6 h-6 text-indigo-600" />
        API Usage
      </h1>

      {/* Summary Card */}
      <div className="bg-white dark:bg-slate-800 rounded-xl border p-6">
        <div className="flex items-center justify-between mb-3">
          <div>
            <p className="text-sm text-gray-500">Daily Usage</p>
            <p className="text-2xl font-bold">
              {data.daily.total_requests.toLocaleString()} / {data.daily.total_limit.toLocaleString()}
            </p>
          </div>
          <span className={`px-3 py-1 rounded-full text-sm font-medium ${
            usagePercent > 90 ? 'bg-red-100 text-red-700' :
            usagePercent > 70 ? 'bg-yellow-100 text-yellow-700' :
            'bg-green-100 text-green-700'
          }`}>
            {usagePercent.toFixed(0)}% used
          </span>
        </div>
        <div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
          <div
            className={`h-full rounded-full transition-all ${
              usagePercent > 90 ? 'bg-red-500' : usagePercent > 70 ? 'bg-yellow-500' : 'bg-indigo-500'
            }`}
            style={{ width: `${Math.min(usagePercent, 100)}%` }}
          />
        </div>
        <p className="text-xs text-gray-400 mt-2">
          Resets at {new Date(data.daily.reset_at).toLocaleString()}
        </p>
      </div>

      {/* Guard Scan & RAG Highlight Cards */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        <div className="bg-white dark:bg-slate-800 rounded-xl border p-4">
          <div className="flex items-center gap-2 mb-2">
            <Shield className="w-5 h-5 text-purple-600" />
            <h3 className="font-semibold">Guard Scan</h3>
          </div>
          <p className="text-2xl font-bold">{data.guard_scan.requests} / {data.guard_scan.limit}</p>
          <p className="text-xs text-gray-500 mt-1">~{data.guard_scan.ai_credits_used.toFixed(1)} AI credits estimated</p>
        </div>
        <div className="bg-white dark:bg-slate-800 rounded-xl border p-4">
          <div className="flex items-center gap-2 mb-2">
            <BookOpen className="w-5 h-5 text-blue-600" />
            <h3 className="font-semibold">RAG Query</h3>
          </div>
          <p className="text-2xl font-bold">{data.rag_query.requests} / {data.rag_query.limit}</p>
          <p className="text-xs text-gray-500 mt-1">~{data.rag_query.estimated_tokens.toLocaleString()} tokens estimated</p>
        </div>
      </div>

      {/* Per-Endpoint Breakdown */}
      <div className="bg-white dark:bg-slate-800 rounded-xl border overflow-hidden">
        <div className="p-4 border-b">
          <h3 className="font-semibold">Endpoint Breakdown</h3>
        </div>
        <table className="w-full text-sm">
          <thead className="bg-gray-50 dark:bg-slate-900">
            <tr>
              <th className="text-left p-3 font-medium">Endpoint</th>
              <th className="text-right p-3 font-medium">Requests</th>
              <th className="text-right p-3 font-medium">Limit</th>
              <th className="text-right p-3 font-medium">Remaining</th>
              <th className="text-right p-3 font-medium">Reset</th>
            </tr>
          </thead>
          <tbody>
            {data.endpoints.map(ep => (
              <tr key={ep.endpoint} className="border-t">
                <td className="p-3">{ep.endpoint}</td>
                <td className="text-right p-3">{ep.requests.toLocaleString()}</td>
                <td className="text-right p-3">{ep.limit.toLocaleString()}</td>
                <td className={`text-right p-3 font-medium ${
                  ep.remaining < 10 ? 'text-red-600' : ep.remaining < 100 ? 'text-yellow-600' : ''
                }`}>{ep.remaining.toLocaleString()}</td>
                <td className="text-right p-3 text-gray-400 text-xs">{new Date(ep.reset_at).toLocaleTimeString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* 7-Day History Chart */}
      <div className="bg-white dark:bg-slate-800 rounded-xl border p-4">
        <h3 className="font-semibold mb-4">7-Day Request History</h3>
        <ResponsiveContainer width="100%" height={200}>
          <LineChart data={data.history}>
            <XAxis dataKey="date" tick={{ fontSize: 11 }} tickFormatter={d => new Date(d).toLocaleDateString('en', { month: 'short', day: 'numeric' })} />
            <YAxis tick={{ fontSize: 11 }} />
            <Tooltip />
            <Line type="monotone" dataKey="requests" stroke="#6366f1" strokeWidth={2} dot={false} />
          </LineChart>
        </ResponsiveContainer>
      </div>

      {/* Recent 429 Events */}
      {data.recent_429s?.length > 0 && (
        <div className="bg-white dark:bg-slate-800 rounded-xl border p-4">
          <h3 className="font-semibold mb-3 flex items-center gap-2">
            <AlertTriangle className="w-4 h-4 text-red-500" />
            Recent Rate Limit Events
          </h3>
          <div className="space-y-2">
            {data.recent_429s.map((event, i) => (
              <div key={i} className="flex items-center justify-between text-sm p-2 bg-red-50 dark:bg-red-900/20 rounded-lg">
                <span className="font-medium">{event.endpoint}</span>
                <span className="text-gray-500 text-xs">{new Date(event.timestamp).toLocaleString()}</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const ApiUsageSkeleton = () => (
  <div className="max-w-5xl mx-auto px-4 py-6 space-y-6 animate-pulse">
    <div className="h-8 w-48 bg-gray-200 rounded" />
    <div className="h-32 bg-gray-100 rounded-xl" />
    <div className="grid grid-cols-2 gap-4">
      <div className="h-24 bg-gray-100 rounded-xl" />
      <div className="h-24 bg-gray-100 rounded-xl" />
    </div>
    <div className="h-48 bg-gray-100 rounded-xl" />
    <div className="h-52 bg-gray-100 rounded-xl" />
  </div>
);

Affected Files

  • backend/app/api/v1/analytics.py — add /usage endpoint
  • backend/app/core/deps.py — add Redis dependency injection (if not already)
  • backend/app/modules/guard/guard.py — log rate limit events to Redis
  • frontend/src/pages/ApiUsagePage.tsx — new API usage page
  • frontend/src/App.tsx — add route for /usage or /api-usage
  • frontend/src/components/layout/Sidebar.tsx — add "API Usage" nav link

Labels

type:feature, level:beginner, GSSoC-26

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions