Add admin user usage analytics endpoint

Implements /admin/users/daily/activity endpoint for admins to view
usage metrics across all users with Claude Code filtering.

Backend changes:
- New endpoint: GET /admin/users/daily/activity
- Returns summary stats, top N users, and paginated user list
- Supports filtering by tags (e.g., User-Agent:claude-code)
- Supports spend thresholds (min/max)
- Supports sorting by spend/requests/tokens
- Pagination with configurable page size
- Admin-only access with permission checks

Frontend changes:
- Added adminUsersDailyActivityCall() in networking.tsx
- Follows same pattern as team/tag/org daily activity calls
- Supports tag filters, spend filters, sorting, pagination

Tests:
- Unit tests for core functionality
- Tests for filtering and pagination
- Mock-based tests for database queries

Endpoint design follows existing /user/daily/activity pattern:
- Single paginated endpoint
- Returns top users for visualization (bar chart)
- Returns paginated users for table display
- Returns summary metrics (totals, averages, counts)

Use cases supported:
- "Show me Claude Code power users" (filter by tag, sort by spend desc)
- "Show me users spending >$200/mo" (min_spend filter)
- "Show me lowest users" (sort by spend asc)

Next steps for UI:
- Create UserUsage component similar to EntityUsage
- Add "User Usage" option to UsageViewSelect dropdown
- Create bar chart component for top users
- Create table component for paginated user list
This commit is contained in:
Claude 2026-01-21 19:37:51 +00:00
parent a02c43d300
commit 86ee31a753
No known key found for this signature in database
4 changed files with 773 additions and 0 deletions

View file

@ -0,0 +1,402 @@
"""
Admin User Usage Endpoints
Provides endpoints for admins to view usage metrics across all users.
Follows the same pattern as /user/daily/activity but for admin-level user analytics.
"""
from datetime import datetime
from typing import Any, Dict, List, Optional
import fastapi
from fastapi import APIRouter, Depends, HTTPException, Query, status
from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.management_endpoints.common_daily_activity import (
SpendAnalyticsPaginatedResponse,
)
from litellm.proxy.utils import PrismaClient
router = APIRouter()
def _user_has_admin_view(user_api_key_dict: UserAPIKeyAuth) -> bool:
"""Check if user has admin permissions."""
from litellm.proxy.proxy_server import premium_user
from litellm.utils.utils import get_utc_datetime
from litellm.proxy.management_endpoints.internal_user_endpoints import (
_user_has_admin_view as internal_admin_check,
)
return internal_admin_check(user_api_key_dict)
async def get_admin_users_usage(
prisma_client: Optional[PrismaClient],
start_date: str,
end_date: str,
tag_filters: Optional[List[str]] = None,
min_spend: Optional[float] = None,
max_spend: Optional[float] = None,
sort_by: str = "spend",
sort_order: str = "desc",
page: int = 1,
page_size: int = 50,
top_n: int = 10,
) -> Dict[str, Any]:
"""
Get admin-level user usage analytics with pagination.
Returns:
- summary: Aggregate stats across all users
- top_users: Top N users for bar chart visualization
- users: Paginated list of users for table
- pagination: Pagination metadata
"""
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
# Build WHERE conditions for queries
where_conditions = []
params_summary = [start_date, end_date]
params_top = [start_date, end_date]
params_paginated = [start_date, end_date]
# Base WHERE clause
base_where = "dus.date >= $1 AND dus.date <= $2 AND vt.user_id IS NOT NULL"
# Add tag filters
tag_filter_clause = ""
if tag_filters and len(tag_filters) > 0:
tag_placeholders = ", ".join([f"${i+3}" for i in range(len(tag_filters))])
tag_filter_clause = f" AND dts.tag IN ({tag_placeholders})"
params_summary.extend(tag_filters)
params_top.extend(tag_filters)
params_paginated.extend(tag_filters)
# Query 1: Get summary statistics
summary_sql = f"""
WITH user_totals AS (
SELECT
vt.user_id,
SUM(dus.spend) as user_spend,
SUM(dus.api_requests) as user_requests,
SUM(dus.successful_requests) as user_successful_requests,
SUM(dus.failed_requests) as user_failed_requests,
SUM(dus.prompt_tokens + dus.completion_tokens) as user_tokens
FROM "LiteLLM_DailyUserSpend" dus
INNER JOIN "LiteLLM_VerificationToken" vt ON dus.api_key = vt.token
{f'INNER JOIN "LiteLLM_DailyTagSpend" dts ON dus.api_key = dts.api_key AND dus.date = dts.date' if tag_filters else ''}
WHERE {base_where}{tag_filter_clause if tag_filters else ''}
GROUP BY vt.user_id
)
SELECT
COUNT(DISTINCT user_id) as total_users,
COALESCE(SUM(user_spend), 0) as total_spend,
COALESCE(SUM(user_requests), 0) as total_requests,
COALESCE(SUM(user_successful_requests), 0) as total_successful_requests,
COALESCE(SUM(user_failed_requests), 0) as total_failed_requests,
COALESCE(SUM(user_tokens), 0) as total_tokens,
COALESCE(AVG(user_spend), 0) as avg_spend_per_user,
COUNT(CASE WHEN user_spend > 200 THEN 1 END) as power_users_count,
COUNT(CASE WHEN user_spend < 10 THEN 1 END) as low_users_count
FROM user_totals
"""
if min_spend is not None:
summary_sql += f" WHERE user_spend >= ${len(params_summary) + 1}"
params_summary.append(min_spend)
if max_spend is not None:
summary_sql += (
f" {'AND' if min_spend is not None else 'WHERE'} user_spend <= ${len(params_summary) + 1}"
)
params_summary.append(max_spend)
summary_result = await prisma_client.db.query_raw(summary_sql, *params_summary)
summary = {
"total_users": summary_result[0].get("total_users", 0) if summary_result else 0,
"total_spend": float(summary_result[0].get("total_spend", 0.0)) if summary_result else 0.0,
"total_requests": int(summary_result[0].get("total_requests", 0)) if summary_result else 0,
"total_successful_requests": int(summary_result[0].get("total_successful_requests", 0))
if summary_result
else 0,
"total_failed_requests": int(summary_result[0].get("total_failed_requests", 0))
if summary_result
else 0,
"total_tokens": int(summary_result[0].get("total_tokens", 0)) if summary_result else 0,
"avg_spend_per_user": float(summary_result[0].get("avg_spend_per_user", 0.0))
if summary_result
else 0.0,
"power_users_count": summary_result[0].get("power_users_count", 0) if summary_result else 0,
"low_users_count": summary_result[0].get("low_users_count", 0) if summary_result else 0,
}
# Query 2: Get top N users for bar chart
sort_field_map = {
"spend": "total_spend",
"requests": "total_requests",
"tokens": "total_tokens",
}
db_sort_field = sort_field_map.get(sort_by, "total_spend")
spend_filter_clause = ""
if min_spend is not None:
spend_filter_clause += f" HAVING SUM(dus.spend) >= ${len(params_top) + 1}"
params_top.append(min_spend)
if max_spend is not None:
spend_filter_clause += f" AND SUM(dus.spend) <= ${len(params_top) + 1}"
params_top.append(max_spend)
elif max_spend is not None:
spend_filter_clause += f" HAVING SUM(dus.spend) <= ${len(params_top) + 1}"
params_top.append(max_spend)
top_users_sql = f"""
SELECT
vt.user_id,
ut.user_email,
SUM(dus.spend) as total_spend,
SUM(dus.api_requests) as total_requests,
SUM(dus.successful_requests) as total_successful_requests,
SUM(dus.failed_requests) as total_failed_requests,
SUM(dus.prompt_tokens) as total_prompt_tokens,
SUM(dus.completion_tokens) as total_completion_tokens,
SUM(dus.prompt_tokens + dus.completion_tokens) as total_tokens,
COUNT(DISTINCT dus.date) as days_active,
MIN(dus.date) as first_request_date,
MAX(dus.date) as last_request_date,
ARRAY_AGG(DISTINCT dts.tag) FILTER (WHERE dts.tag IS NOT NULL) as tags,
ARRAY_AGG(DISTINCT dus.model) FILTER (WHERE dus.model IS NOT NULL) as models_used
FROM "LiteLLM_DailyUserSpend" dus
INNER JOIN "LiteLLM_VerificationToken" vt ON dus.api_key = vt.token
LEFT JOIN "LiteLLM_UserTable" ut ON vt.user_id = ut.user_id
LEFT JOIN "LiteLLM_DailyTagSpend" dts ON dus.api_key = dts.api_key AND dus.date = dts.date
WHERE {base_where}{tag_filter_clause if tag_filters else ''}
GROUP BY vt.user_id, ut.user_email
{spend_filter_clause}
ORDER BY {db_sort_field} {sort_order.upper()}
LIMIT {top_n}
"""
top_users_result = await prisma_client.db.query_raw(top_users_sql, *params_top)
top_users = [
{
"user_id": row["user_id"],
"user_email": row["user_email"] or row["user_id"],
"spend": float(row["total_spend"]),
"requests": int(row["total_requests"]),
"successful_requests": int(row["total_successful_requests"]),
"failed_requests": int(row["total_failed_requests"]),
"prompt_tokens": int(row["total_prompt_tokens"]),
"completion_tokens": int(row["total_completion_tokens"]),
"tokens": int(row["total_tokens"]),
"days_active": int(row["days_active"]),
"first_request_date": str(row["first_request_date"]),
"last_request_date": str(row["last_request_date"]),
"tags": row["tags"] or [],
"models_used": row["models_used"] or [],
}
for row in top_users_result
]
# Query 3: Get paginated users for table
offset = (page - 1) * page_size
# First get total count for pagination
count_sql = f"""
SELECT COUNT(DISTINCT vt.user_id) as total_count
FROM "LiteLLM_DailyUserSpend" dus
INNER JOIN "LiteLLM_VerificationToken" vt ON dus.api_key = vt.token
{f'INNER JOIN "LiteLLM_DailyTagSpend" dts ON dus.api_key = dts.api_key AND dus.date = dts.date' if tag_filters else ''}
WHERE {base_where}{tag_filter_clause if tag_filters else ''}
"""
count_result = await prisma_client.db.query_raw(count_sql, *params_paginated[: len(params_paginated) - (2 if min_spend or max_spend else 0)])
total_count = count_result[0]["total_count"] if count_result else 0
# Get paginated users
paginated_sql = f"""
SELECT
vt.user_id,
ut.user_email,
SUM(dus.spend) as total_spend,
SUM(dus.api_requests) as total_requests,
SUM(dus.successful_requests) as total_successful_requests,
SUM(dus.failed_requests) as total_failed_requests,
SUM(dus.prompt_tokens) as total_prompt_tokens,
SUM(dus.completion_tokens) as total_completion_tokens,
SUM(dus.prompt_tokens + dus.completion_tokens) as total_tokens,
COUNT(DISTINCT dus.date) as days_active,
MIN(dus.date) as first_request_date,
MAX(dus.date) as last_request_date,
ARRAY_AGG(DISTINCT dts.tag) FILTER (WHERE dts.tag IS NOT NULL) as tags,
ARRAY_AGG(DISTINCT dus.model) FILTER (WHERE dus.model IS NOT NULL) as models_used
FROM "LiteLLM_DailyUserSpend" dus
INNER JOIN "LiteLLM_VerificationToken" vt ON dus.api_key = vt.token
LEFT JOIN "LiteLLM_UserTable" ut ON vt.user_id = ut.user_id
LEFT JOIN "LiteLLM_DailyTagSpend" dts ON dus.api_key = dts.api_key AND dus.date = dts.date
WHERE {base_where}{tag_filter_clause if tag_filters else ''}
GROUP BY vt.user_id, ut.user_email
{spend_filter_clause}
ORDER BY {db_sort_field} {sort_order.upper()}
LIMIT {page_size} OFFSET {offset}
"""
paginated_result = await prisma_client.db.query_raw(paginated_sql, *params_paginated)
users = [
{
"user_id": row["user_id"],
"user_email": row["user_email"] or row["user_id"],
"spend": float(row["total_spend"]),
"requests": int(row["total_requests"]),
"successful_requests": int(row["total_successful_requests"]),
"failed_requests": int(row["total_failed_requests"]),
"prompt_tokens": int(row["total_prompt_tokens"]),
"completion_tokens": int(row["total_completion_tokens"]),
"tokens": int(row["total_tokens"]),
"days_active": int(row["days_active"]),
"first_request_date": str(row["first_request_date"]),
"last_request_date": str(row["last_request_date"]),
"tags": row["tags"] or [],
"models_used": row["models_used"] or [],
}
for row in paginated_result
]
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 0
return {
"summary": summary,
"top_users": top_users,
"users": users,
"pagination": {
"page": page,
"page_size": page_size,
"total_count": total_count,
"total_pages": total_pages,
},
}
@router.get(
"/admin/users/daily/activity",
tags=["Budget & Spend Tracking", "Admin User Analytics"],
dependencies=[Depends(user_api_key_auth)],
)
async def get_admin_users_daily_activity(
start_date: str = fastapi.Query(
...,
description="Start date in YYYY-MM-DD format",
),
end_date: str = fastapi.Query(
...,
description="End date in YYYY-MM-DD format",
),
tag_filters: Optional[List[str]] = fastapi.Query(
default=None,
description="Filter by specific tags (e.g., User-Agent:claude-code)",
),
min_spend: Optional[float] = fastapi.Query(
default=None,
description="Minimum spend threshold",
),
max_spend: Optional[float] = fastapi.Query(
default=None,
description="Maximum spend threshold",
),
sort_by: str = fastapi.Query(
default="spend",
description="Sort by field: spend, requests, or tokens",
),
sort_order: str = fastapi.Query(
default="desc",
description="Sort order: asc or desc",
),
page: int = fastapi.Query(
default=1, description="Page number for pagination", ge=1
),
page_size: int = fastapi.Query(
default=50, description="Items per page", ge=1, le=100
),
top_n: int = fastapi.Query(
default=10, description="Number of top users to return", ge=1, le=50
),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Get admin-level user usage analytics with pagination.
Returns usage metrics for all users with:
- Summary: Aggregate statistics across all users
- Top Users: Top N users for visualization (bar chart)
- Users: Paginated list of users for table display
- Pagination: Metadata for pagination
Requires admin permissions.
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
# Check admin permissions
if not _user_has_admin_view(user_api_key_dict):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"error": "Admin permissions required"},
)
# Validate date format
try:
datetime.strptime(start_date, "%Y-%m-%d")
datetime.strptime(end_date, "%Y-%m-%d")
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"error": "Invalid date format. Use YYYY-MM-DD"},
)
# Validate sort parameters
if sort_by not in ["spend", "requests", "tokens"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"error": "sort_by must be one of: spend, requests, tokens"},
)
if sort_order not in ["asc", "desc"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"error": "sort_order must be one of: asc, desc"},
)
try:
result = await get_admin_users_usage(
prisma_client=prisma_client,
start_date=start_date,
end_date=end_date,
tag_filters=tag_filters,
min_spend=min_spend,
max_spend=max_spend,
sort_by=sort_by,
sort_order=sort_order,
page=page,
page_size=page_size,
top_n=top_n,
)
return result
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"error": f"Failed to fetch admin user usage: {str(e)}"},
)

View file

@ -309,6 +309,9 @@ from litellm.proxy.management_endpoints.internal_user_endpoints import (
from litellm.proxy.management_endpoints.internal_user_endpoints import (
user_update,
)
from litellm.proxy.management_endpoints.admin_user_usage_endpoints import (
router as admin_user_usage_router,
)
from litellm.proxy.management_endpoints.key_management_endpoints import (
delete_verification_tokens,
duration_in_seconds,
@ -10509,6 +10512,7 @@ app.include_router(pass_through_router)
app.include_router(health_router)
app.include_router(key_management_router)
app.include_router(internal_user_router)
app.include_router(admin_user_usage_router)
app.include_router(team_router)
app.include_router(ui_sso_router)
app.include_router(scim_router)

View file

@ -0,0 +1,334 @@
"""
Tests for Admin User Usage Analytics Endpoint
"""
import pytest
import sys
import os
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch
# Add parent directory to path
sys.path.insert(0, os.path.abspath("../.."))
from litellm.proxy.management_endpoints.admin_user_usage_endpoints import (
get_admin_users_usage,
)
@pytest.mark.asyncio
async def test_get_admin_users_usage_basic():
"""Test basic admin user usage query with mock data"""
# Mock prisma client
mock_prisma = MagicMock()
mock_db = MagicMock()
mock_prisma.db = mock_db
# Mock summary query result
mock_db.query_raw = AsyncMock()
# First call: summary query
mock_db.query_raw.side_effect = [
# Summary result
[
{
"total_users": 10,
"total_spend": 1000.0,
"total_requests": 5000,
"total_successful_requests": 4900,
"total_failed_requests": 100,
"total_tokens": 100000,
"avg_spend_per_user": 100.0,
"power_users_count": 2,
"low_users_count": 3,
}
],
# Top users result
[
{
"user_id": "user_1",
"user_email": "user1@example.com",
"total_spend": 500.0,
"total_requests": 2000,
"total_successful_requests": 1980,
"total_failed_requests": 20,
"total_prompt_tokens": 40000,
"total_completion_tokens": 10000,
"total_tokens": 50000,
"days_active": 15,
"first_request_date": "2026-01-01",
"last_request_date": "2026-01-15",
"tags": ["User-Agent:claude-code"],
"models_used": ["claude-sonnet-4"],
}
],
# Count query for pagination
[{"total_count": 10}],
# Paginated users result
[
{
"user_id": "user_1",
"user_email": "user1@example.com",
"total_spend": 500.0,
"total_requests": 2000,
"total_successful_requests": 1980,
"total_failed_requests": 20,
"total_prompt_tokens": 40000,
"total_completion_tokens": 10000,
"total_tokens": 50000,
"days_active": 15,
"first_request_date": "2026-01-01",
"last_request_date": "2026-01-15",
"tags": ["User-Agent:claude-code"],
"models_used": ["claude-sonnet-4"],
},
{
"user_id": "user_2",
"user_email": "user2@example.com",
"total_spend": 300.0,
"total_requests": 1500,
"total_successful_requests": 1490,
"total_failed_requests": 10,
"total_prompt_tokens": 30000,
"total_completion_tokens": 7500,
"total_tokens": 37500,
"days_active": 12,
"first_request_date": "2026-01-03",
"last_request_date": "2026-01-15",
"tags": ["User-Agent:claude-code"],
"models_used": ["claude-opus-4"],
},
],
]
# Call the function
result = await get_admin_users_usage(
prisma_client=mock_prisma,
start_date="2026-01-01",
end_date="2026-01-15",
tag_filters=["User-Agent:claude-code"],
min_spend=None,
max_spend=None,
sort_by="spend",
sort_order="desc",
page=1,
page_size=50,
top_n=10,
)
# Assertions
assert result is not None
assert "summary" in result
assert "top_users" in result
assert "users" in result
assert "pagination" in result
# Check summary
assert result["summary"]["total_users"] == 10
assert result["summary"]["total_spend"] == 1000.0
assert result["summary"]["power_users_count"] == 2
# Check top users
assert len(result["top_users"]) == 1
assert result["top_users"][0]["user_email"] == "user1@example.com"
assert result["top_users"][0]["spend"] == 500.0
# Check paginated users
assert len(result["users"]) == 2
assert result["users"][0]["user_email"] == "user1@example.com"
assert result["users"][1]["user_email"] == "user2@example.com"
# Check pagination
assert result["pagination"]["page"] == 1
assert result["pagination"]["total_count"] == 10
assert result["pagination"]["total_pages"] == 1
@pytest.mark.asyncio
async def test_get_admin_users_usage_with_filters():
"""Test admin user usage with spend filters"""
# Mock prisma client
mock_prisma = MagicMock()
mock_db = MagicMock()
mock_prisma.db = mock_db
mock_db.query_raw = AsyncMock()
# Mock results for filtered query
mock_db.query_raw.side_effect = [
# Summary result
[
{
"total_users": 2,
"total_spend": 800.0,
"total_requests": 3500,
"total_successful_requests": 3470,
"total_failed_requests": 30,
"total_tokens": 87500,
"avg_spend_per_user": 400.0,
"power_users_count": 2,
"low_users_count": 0,
}
],
# Top users result
[
{
"user_id": "user_1",
"user_email": "user1@example.com",
"total_spend": 500.0,
"total_requests": 2000,
"total_successful_requests": 1980,
"total_failed_requests": 20,
"total_prompt_tokens": 40000,
"total_completion_tokens": 10000,
"total_tokens": 50000,
"days_active": 15,
"first_request_date": "2026-01-01",
"last_request_date": "2026-01-15",
"tags": ["User-Agent:claude-code"],
"models_used": ["claude-sonnet-4"],
}
],
# Count query
[{"total_count": 2}],
# Paginated users result
[
{
"user_id": "user_1",
"user_email": "user1@example.com",
"total_spend": 500.0,
"total_requests": 2000,
"total_successful_requests": 1980,
"total_failed_requests": 20,
"total_prompt_tokens": 40000,
"total_completion_tokens": 10000,
"total_tokens": 50000,
"days_active": 15,
"first_request_date": "2026-01-01",
"last_request_date": "2026-01-15",
"tags": ["User-Agent:claude-code"],
"models_used": ["claude-sonnet-4"],
},
{
"user_id": "user_2",
"user_email": "user2@example.com",
"total_spend": 300.0,
"total_requests": 1500,
"total_successful_requests": 1490,
"total_failed_requests": 10,
"total_prompt_tokens": 30000,
"total_completion_tokens": 7500,
"total_tokens": 37500,
"days_active": 12,
"first_request_date": "2026-01-03",
"last_request_date": "2026-01-15",
"tags": ["User-Agent:claude-code"],
"models_used": ["claude-opus-4"],
},
],
]
# Call with min_spend filter
result = await get_admin_users_usage(
prisma_client=mock_prisma,
start_date="2026-01-01",
end_date="2026-01-15",
tag_filters=["User-Agent:claude-code"],
min_spend=200.0, # Filter for users spending > $200
max_spend=None,
sort_by="spend",
sort_order="desc",
page=1,
page_size=50,
top_n=10,
)
# Assertions
assert result is not None
assert result["summary"]["total_users"] == 2
assert result["summary"]["power_users_count"] == 2
assert len(result["users"]) == 2
# All users should have spend >= 200
for user in result["users"]:
assert user["spend"] >= 200.0
@pytest.mark.asyncio
async def test_get_admin_users_usage_pagination():
"""Test pagination works correctly"""
# Mock prisma client
mock_prisma = MagicMock()
mock_db = MagicMock()
mock_prisma.db = mock_db
mock_db.query_raw = AsyncMock()
# Mock results for page 2
mock_db.query_raw.side_effect = [
# Summary result
[
{
"total_users": 150,
"total_spend": 15000.0,
"total_requests": 75000,
"total_successful_requests": 74000,
"total_failed_requests": 1000,
"total_tokens": 1500000,
"avg_spend_per_user": 100.0,
"power_users_count": 25,
"low_users_count": 30,
}
],
# Top users result
[],
# Count query
[{"total_count": 150}],
# Paginated users result (page 2: users 51-100)
[
{
"user_id": f"user_{i}",
"user_email": f"user{i}@example.com",
"total_spend": 100.0 - i,
"total_requests": 500,
"total_successful_requests": 490,
"total_failed_requests": 10,
"total_prompt_tokens": 10000,
"total_completion_tokens": 5000,
"total_tokens": 15000,
"days_active": 10,
"first_request_date": "2026-01-01",
"last_request_date": "2026-01-15",
"tags": [],
"models_used": ["claude-sonnet-4"],
}
for i in range(51, 101)
],
]
# Call with page 2
result = await get_admin_users_usage(
prisma_client=mock_prisma,
start_date="2026-01-01",
end_date="2026-01-15",
tag_filters=None,
min_spend=None,
max_spend=None,
sort_by="spend",
sort_order="desc",
page=2,
page_size=50,
top_n=10,
)
# Assertions
assert result["pagination"]["page"] == 2
assert result["pagination"]["page_size"] == 50
assert result["pagination"]["total_count"] == 150
assert result["pagination"]["total_pages"] == 3
assert len(result["users"]) == 50
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View file

@ -1872,6 +1872,39 @@ export const agentDailyActivityCall = async (
});
};
export const adminUsersDailyActivityCall = async (
accessToken: string,
startTime: Date,
endTime: Date,
page: number = 1,
tagFilters: string[] | null = null,
minSpend: number | null = null,
maxSpend: number | null = null,
sortBy: string = "spend",
sortOrder: string = "desc",
topN: number = 10,
) => {
/**
* Get admin-level user usage analytics
* Returns top users, paginated user list, and summary stats
*/
return fetchDailyActivity({
accessToken,
endpoint: "/admin/users/daily/activity",
startTime,
endTime,
page,
extraQueryParams: {
tag_filters: tagFilters,
min_spend: minSpend,
max_spend: maxSpend,
sort_by: sortBy,
sort_order: sortOrder,
top_n: topN,
},
});
};
export const getTotalSpendCall = async (accessToken: string) => {
/**
* Get all models on proxy