[Feat] v2 updates - tracking DAU, WAU, MAU for coding tool usage + show Daily Usage per User (#13147)

* Add user agent analytics endpoints and UI for tracking client metrics

Co-authored-by: ishaan <ishaan@berri.ai>

* fix user agent analytics

* fix getting DAU

* fixes for user agent

* showing top user agents

* on this page remove Success Rate by User Agent

* fix linting

* add agent activity

* cleanup interface

* fix ruff

* round cost

* fix charts

* fixes - show DAU, MAU, WAU

* move to a diff file

* fix

* fixes for user agent analytics

* fix user_agent_analytics_endpoints

* fix mypy linting

* fix linting

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: ishaan <ishaan@berri.ai>
This commit is contained in:
Ishaan Jaff
2025-07-30 14:42:30 -07:00
committed by GitHub
co-authored by Cursor Agent ishaan
parent eb8a338d9b
commit 92cdac2e87
4 changed files with 688 additions and 192 deletions
@@ -14,7 +14,7 @@ tracked by the system.
"""
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Set
from typing import Dict, List, Optional, Set, cast
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
@@ -59,6 +59,27 @@ class UserAgentAnalyticsResponse(BaseModel):
total_pages: int
class PerUserMetrics(BaseModel):
"""Metrics for individual user"""
user_id: str
user_email: Optional[str] = None
user_agent: Optional[str] = None
successful_requests: int = 0
failed_requests: int = 0
total_requests: int = 0
total_tokens: int = 0
spend: float = 0.0
class PerUserAnalyticsResponse(BaseModel):
"""Response for per-user analytics"""
results: List[PerUserMetrics]
total_count: int
page: int
page_size: int
total_pages: int
async def _get_unique_users_for_tags(
prisma_client,
tags: List[str],
@@ -279,7 +300,7 @@ async def get_user_agent_analytics(
daily_data_by_tag_and_date[tag][date_str] = daily_data
# Calculate DAU/WAU/MAU for each date and tag combination
unique_dates = set()
unique_dates: set[str] = set()
for tag_data in daily_data_by_tag_and_date.values():
unique_dates.update(tag_data.keys())
@@ -455,7 +476,7 @@ async def get_user_agent_summary(
}
for ua, metrics in user_agent_totals.items()
],
key=lambda x: x["requests"],
key=lambda x: cast(int, x["requests"]),
reverse=True,
)[:10] # Top 10
@@ -473,4 +494,177 @@ async def get_user_agent_summary(
raise HTTPException(
status_code=500,
detail=f"Failed to fetch user agent summary: {str(e)}",
)
@router.get(
"/tag/user-agent/per-user-analytics",
response_model=PerUserAnalyticsResponse,
tags=["tag management", "user agent analytics"],
dependencies=[Depends(user_api_key_auth)],
)
async def get_per_user_analytics(
start_date: Optional[str] = Query(
default=None,
description="Start date in YYYY-MM-DD format",
),
end_date: Optional[str] = Query(
default=None,
description="End date in YYYY-MM-DD format",
),
page: int = Query(default=1, description="Page number for pagination", ge=1),
page_size: int = Query(
default=50, description="Items per page", ge=1, le=1000
),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Get per-user analytics including successful requests, tokens, and spend by individual users.
This endpoint provides usage metrics broken down by individual users based on their
user-agent activity during the specified time period.
Args:
start_date: Start date for the analytics period (YYYY-MM-DD)
end_date: End date for the analytics period (YYYY-MM-DD)
page: Page number for pagination
page_size: Number of items per page
Returns:
PerUserAnalyticsResponse: Analytics data broken down by individual users
"""
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},
)
if start_date is None or end_date is None:
raise HTTPException(
status_code=400,
detail={"error": "Please provide start_date and end_date"},
)
try:
# Get all user-agent tags from the database
user_agent_tags_records = await prisma_client.db.litellm_dailytagspend.find_many(
where={
"tag": {"startswith": "User-Agent: "},
"date": {"gte": start_date, "lte": end_date},
},
distinct=["tag"],
)
user_agent_tags = [record.tag for record in user_agent_tags_records]
if not user_agent_tags:
return PerUserAnalyticsResponse(
results=[],
total_count=0,
page=page,
page_size=page_size,
total_pages=0,
)
# Get all records for user-agent tags in the date range
tag_records = await prisma_client.db.litellm_dailytagspend.find_many(
where={
"tag": {"in": user_agent_tags},
"date": {"gte": start_date, "lte": end_date}
}
)
# Get unique api_keys
api_keys = set(record.api_key for record in tag_records if record.api_key)
if not api_keys:
return PerUserAnalyticsResponse(
results=[],
total_count=0,
page=page,
page_size=page_size,
total_pages=0,
)
# Lookup user_id for each api_key
api_key_records = await prisma_client.db.litellm_verificationtoken.find_many(
where={"token": {"in": list(api_keys)}}
)
# Create mapping from api_key to user_id
api_key_to_user_id = {
record.token: record.user_id
for record in api_key_records
if record.user_id
}
# Get user emails for the user_ids
user_ids = list(set(api_key_to_user_id.values()))
user_records = await prisma_client.db.litellm_usertable.find_many(
where={"user_id": {"in": user_ids}}
)
# Create mapping from user_id to user_email
user_id_to_email = {
record.user_id: record.user_email
for record in user_records
}
# Aggregate metrics by user
user_metrics: Dict[str, PerUserMetrics] = {}
for record in tag_records:
if record.api_key in api_key_to_user_id:
user_id = api_key_to_user_id[record.api_key]
user_agent = _extract_user_agent_from_tag(record.tag)
if user_id not in user_metrics:
user_metrics[user_id] = PerUserMetrics(
user_id=user_id,
user_email=user_id_to_email.get(user_id),
user_agent=user_agent
)
else:
# If user agent is different, keep the first one or prioritize certain ones
if user_agent and not user_metrics[user_id].user_agent:
user_metrics[user_id].user_agent = user_agent
# Aggregate metrics
user_metrics[user_id].successful_requests += record.successful_requests or 0
user_metrics[user_id].failed_requests += record.failed_requests or 0
user_metrics[user_id].total_requests += record.api_requests or 0
# Calculate total_tokens from prompt_tokens + completion_tokens
prompt_tokens = record.prompt_tokens or 0
completion_tokens = record.completion_tokens or 0
user_metrics[user_id].total_tokens += int(prompt_tokens + completion_tokens)
user_metrics[user_id].spend += record.spend or 0.0
# Convert to list and sort by successful requests (descending)
results = sorted(
list(user_metrics.values()),
key=lambda x: x.successful_requests,
reverse=True
)
# Apply pagination
total_count = len(results)
total_pages = (total_count + page_size - 1) // page_size
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
paginated_results = results[start_idx:end_idx]
return PerUserAnalyticsResponse(
results=paginated_results,
total_count=total_count,
page=page,
page_size=page_size,
total_pages=total_pages,
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to fetch per-user analytics: {str(e)}",
)
@@ -6551,3 +6551,60 @@ export const userAgentSummaryCall = async (
}
};
export const perUserAnalyticsCall = async (
accessToken: string,
startTime: Date,
endTime: Date,
page: number = 1,
pageSize: number = 50
) => {
/**
* Get per-user analytics data
*/
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/tag/user-agent/per-user-analytics`
: `/tag/user-agent/per-user-analytics`;
const queryParams = new URLSearchParams();
// Format dates as YYYY-MM-DD for the API
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
queryParams.append("start_date", formatDate(startTime));
queryParams.append("end_date", formatDate(endTime));
queryParams.append("page", page.toString());
queryParams.append("page_size", pageSize.toString());
const queryString = queryParams.toString();
if (queryString) {
url += `?${queryString}`;
}
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch per-user analytics:", error);
throw error;
}
};
@@ -0,0 +1,258 @@
import React, { useState, useEffect } from "react";
import {
Card,
Title,
Subtitle,
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
BarChart,
Text,
Button,
Tab,
TabGroup,
TabList,
TabPanel,
TabPanels,
} from "@tremor/react";
import { perUserAnalyticsCall } from "./networking";
import { DateRangePickerValue } from "@tremor/react";
interface PerUserMetrics {
user_id: string;
user_email: string | null;
user_agent: string | null;
successful_requests: number;
failed_requests: number;
total_requests: number;
total_tokens: number;
spend: number;
}
interface PerUserAnalyticsResponse {
results: PerUserMetrics[];
total_count: number;
page: number;
page_size: number;
total_pages: number;
}
interface PerUserUsageProps {
accessToken: string | null;
dateValue: DateRangePickerValue;
formatAbbreviatedNumber: (value: number, decimalPlaces?: number) => string;
}
const PerUserUsage: React.FC<PerUserUsageProps> = ({
accessToken,
dateValue,
formatAbbreviatedNumber,
}) => {
const [perUserData, setPerUserData] = useState<PerUserAnalyticsResponse>({
results: [],
total_count: 0,
page: 1,
page_size: 50,
total_pages: 0,
});
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const fetchPerUserData = async () => {
if (!accessToken || !dateValue.from || !dateValue.to) return;
setLoading(true);
try {
const response = await perUserAnalyticsCall(
accessToken,
dateValue.from,
dateValue.to,
currentPage,
50
);
setPerUserData(response);
} catch (error) {
console.error("Failed to fetch per-user data:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPerUserData();
}, [accessToken, dateValue, currentPage]);
const handleNextPage = () => {
if (currentPage < perUserData.total_pages) {
setCurrentPage(currentPage + 1);
}
};
const handlePrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
return (
<div className="mb-6">
<Title>Per User Usage</Title>
<Subtitle>Individual developer usage metrics</Subtitle>
<TabGroup>
<TabList className="mb-6">
<Tab>User Details</Tab>
<Tab>Usage Distribution</Tab>
</TabList>
<TabPanels>
{/* Tab 1: Existing User Details Table */}
<TabPanel>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>User ID</TableHeaderCell>
<TableHeaderCell>User Email</TableHeaderCell>
<TableHeaderCell>User Agent</TableHeaderCell>
<TableHeaderCell className="text-right">Success Generations</TableHeaderCell>
<TableHeaderCell className="text-right">Total Tokens</TableHeaderCell>
<TableHeaderCell className="text-right">Failed Requests</TableHeaderCell>
<TableHeaderCell className="text-right">Total Cost</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{perUserData.results.slice(0, 10).map((item: PerUserMetrics, index: number) => (
<TableRow key={index}>
<TableCell>
<Text className="font-medium">{item.user_id}</Text>
</TableCell>
<TableCell>
<Text>{item.user_email || "N/A"}</Text>
</TableCell>
<TableCell>
<Text>{item.user_agent || "Unknown"}</Text>
</TableCell>
<TableCell className="text-right">
<Text>{formatAbbreviatedNumber(item.successful_requests)}</Text>
</TableCell>
<TableCell className="text-right">
<Text>{formatAbbreviatedNumber(item.total_tokens)}</Text>
</TableCell>
<TableCell className="text-right">
<Text>{formatAbbreviatedNumber(item.failed_requests)}</Text>
</TableCell>
<TableCell className="text-right">
<Text>${formatAbbreviatedNumber(item.spend, 4)}</Text>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{perUserData.results.length > 10 && (
<div className="mt-4 flex justify-between items-center">
<Text className="text-sm text-gray-500">
Showing 10 of {perUserData.total_count} results
</Text>
<div className="flex gap-2">
<Button
size="sm"
variant="secondary"
onClick={handlePrevPage}
disabled={currentPage === 1}
>
Previous
</Button>
<Button
size="sm"
variant="secondary"
onClick={handleNextPage}
disabled={currentPage >= perUserData.total_pages}
>
Next
</Button>
</div>
</div>
)}
</TabPanel>
{/* Tab 2: Usage Distribution Histogram */}
<TabPanel>
<div className="mb-4">
<Title className="text-lg">User Usage Distribution</Title>
<Subtitle>Number of users by successful request frequency</Subtitle>
</div>
<BarChart
data={(() => {
// Categorize users by successful request count and user agent
const categories = {
"1-9 requests": { range: [1, 9], agents: {} as Record<string, number> },
"10-99 requests": { range: [10, 99], agents: {} as Record<string, number> },
"100-999 requests": { range: [100, 999], agents: {} as Record<string, number> },
"1K-9.9K requests": { range: [1000, 9999], agents: {} as Record<string, number> },
"10K-99.9K requests": { range: [10000, 99999], agents: {} as Record<string, number> },
"100K+ requests": { range: [100000, Infinity], agents: {} as Record<string, number> },
};
// Count users in each category by user agent
perUserData.results.forEach((item: PerUserMetrics) => {
const successCount = item.successful_requests;
const userAgent = item.user_agent || "Unknown";
Object.entries(categories).forEach(([categoryName, category]) => {
if (successCount >= category.range[0] && successCount <= category.range[1]) {
if (!category.agents[userAgent]) {
category.agents[userAgent] = 0;
}
category.agents[userAgent]++;
}
});
});
// Get all unique user agents
const allUserAgents = new Set<string>();
Object.values(categories).forEach(category => {
Object.keys(category.agents).forEach(agent => allUserAgents.add(agent));
});
const userAgentsList = Array.from(allUserAgents).sort();
// Convert to chart data format for stacked bar chart
return Object.entries(categories).map(([categoryName, category]) => {
const dataPoint: Record<string, any> = { category: categoryName };
// Add count for each user agent
userAgentsList.forEach(agent => {
dataPoint[agent] = category.agents[agent] || 0;
});
return dataPoint;
});
})()}
index="category"
categories={(() => {
// Get all unique user agents for the chart categories
const allUserAgents = new Set<string>();
perUserData.results.forEach((item: PerUserMetrics) => {
allUserAgents.add(item.user_agent || "Unknown");
});
return Array.from(allUserAgents).sort();
})()}
colors={["blue", "green", "orange", "red", "purple", "yellow", "pink", "indigo"]}
valueFormatter={(value: number) => `${value} users`}
yAxisWidth={80}
showLegend={true}
stack={true}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
);
};
export default PerUserUsage;
@@ -18,10 +18,16 @@ import {
Select,
SelectItem,
Button,
Tab,
TabGroup,
TabList,
TabPanel,
TabPanels,
} from "@tremor/react";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { userAgentAnalyticsCall, userAgentSummaryCall } from "./networking";
import UsageDatePicker from "./shared/usage_date_picker";
import PerUserUsage from "./per_user_usage";
import { DateRangePickerValue } from "@tremor/react";
interface UserAgentMetrics {
@@ -184,20 +190,71 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
total_requests: ua.requests,
}));
// Format numbers with K, M abbreviations
const formatAbbreviatedNumber = (value: number): string => {
if (value >= 100000000) {
return (value / 1000000).toFixed(0) + 'M';
} else if (value >= 10000000) {
return (value / 1000000).toFixed(0) + 'M';
} else if (value >= 1000000) {
return (value / 1000000).toFixed(0) + 'M';
} else if (value >= 10000) {
return (value / 1000).toFixed(0) + 'K';
} else if (value >= 1000) {
return (value / 1000).toFixed(0) + 'K';
// Get unique user agents for chart
const uniqueUserAgents = Array.from(
new Set(analyticsData.results.map(item => item.user_agent || "Unknown"))
).slice(0, 3); // Top 3 user agents
// Prepare daily chart data (DAU)
const dailyChartData = analyticsData.results.reduce((acc, item) => {
const existingDate = acc.find(d => d.date === item.date);
if (existingDate) {
existingDate[item.user_agent || "Unknown"] = item.metrics.dau;
} else {
return value.toString();
const newDateEntry: any = {
date: item.date,
[item.user_agent || "Unknown"]: item.metrics.dau
};
acc.push(newDateEntry);
}
return acc;
}, [] as any[]);
// Prepare weekly chart data (WAU)
const weeklyChartData = analyticsData.results.reduce((acc, item) => {
const existingDate = acc.find(d => d.week === item.date);
if (existingDate) {
existingDate[item.user_agent || "Unknown"] = item.metrics.wau;
} else {
const newDateEntry: any = {
week: `Week ${acc.length + 1}`,
[item.user_agent || "Unknown"]: item.metrics.wau
};
acc.push(newDateEntry);
}
return acc;
}, [] as any[]);
// Prepare monthly chart data (MAU)
const monthlyChartData = analyticsData.results.reduce((acc, item) => {
const existingDate = acc.find(d => d.month === item.date);
if (existingDate) {
existingDate[item.user_agent || "Unknown"] = item.metrics.mau;
} else {
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"];
const newDateEntry: any = {
month: monthNames[acc.length % 7] || `Month ${acc.length + 1}`,
[item.user_agent || "Unknown"]: item.metrics.mau
};
acc.push(newDateEntry);
}
return acc;
}, [] as any[]);
// Format numbers with K, M abbreviations
const formatAbbreviatedNumber = (value: number, decimalPlaces: number = 0): string => {
if (value >= 100000000) {
return (value / 1000000).toFixed(decimalPlaces) + 'M';
} else if (value >= 10000000) {
return (value / 1000000).toFixed(decimalPlaces) + 'M';
} else if (value >= 1000000) {
return (value / 1000000).toFixed(decimalPlaces) + 'M';
} else if (value >= 10000) {
return (value / 1000).toFixed(decimalPlaces) + 'K';
} else if (value >= 1000) {
return (value / 1000).toFixed(decimalPlaces) + 'K';
} else {
return value.toFixed(decimalPlaces);
}
};
@@ -238,196 +295,126 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
<Title className="truncate" title={ua.user_agent}>
{ua.user_agent}
</Title>
<Metric className="text-green-600">
{formatAbbreviatedNumber(ua.successful_requests)}
</Metric>
<Subtitle>Successful Requests</Subtitle>
<Text className="mt-2">
<span className="font-semibold">{formatAbbreviatedNumber(ua.tokens)}</span> tokens
</Text>
<div className="mt-4 space-y-3">
<div>
<Text className="text-sm text-gray-600">Success Requests</Text>
<Metric className="text-lg">{formatAbbreviatedNumber(ua.successful_requests)}</Metric>
</div>
<div>
<Text className="text-sm text-gray-600">Total Tokens</Text>
<Metric className="text-lg">{formatAbbreviatedNumber(ua.tokens)}</Metric>
</div>
<div>
<Text className="text-sm text-gray-600">Total Cost</Text>
<Metric className="text-lg">${formatAbbreviatedNumber(ua.spend, 4)}</Metric>
</div>
</div>
</Card>
))}
{/* Fill remaining slots if less than 4 agents */}
{Array.from({ length: Math.max(0, 4 - summaryData.top_user_agents.length) }).map((_, index) => (
<Card key={`empty-${index}`}>
<Title>No Data</Title>
<Metric>-</Metric>
<Subtitle>Successful Requests</Subtitle>
<Text className="mt-2">
<span className="font-semibold">-</span> tokens
</Text>
<div className="mt-4 space-y-3">
<div>
<Text className="text-sm text-gray-600">Success Requests</Text>
<Metric className="text-lg">-</Metric>
</div>
<div>
<Text className="text-sm text-gray-600">Total Tokens</Text>
<Metric className="text-lg">-</Metric>
</div>
<div>
<Text className="text-sm text-gray-600">Total Cost</Text>
<Metric className="text-lg">-</Metric>
</div>
</div>
</Card>
))}
</Grid>
{/* Charts */}
<Grid numItems={1} className="gap-4">
<Card>
<Title>Success Generations by User Agent</Title>
<BarChart
data={chartData.slice(0, 10)} // Top 10
index="user_agent"
categories={["successful_requests", "total_tokens"]}
colors={["green", "blue"]}
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
yAxisWidth={60}
showLegend={true}
/>
</Card>
</Grid>
{/* Top User Agents Table */}
{/* Main TabGroup for DAU/WAU/MAU vs Per User Usage */}
<Card>
<Title>Top User Agents</Title>
<Table className="mt-4">
<TableHead>
<TableRow>
<TableHeaderCell>User Agent</TableHeaderCell>
<TableHeaderCell className="text-right">Requests</TableHeaderCell>
<TableHeaderCell className="text-right">Success Rate</TableHeaderCell>
<TableHeaderCell className="text-right">Tokens</TableHeaderCell>
<TableHeaderCell className="text-right">Spend</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{summaryData.top_user_agents.map((ua, index) => (
<TableRow key={index}>
<TableCell className="font-medium">{ua.user_agent}</TableCell>
<TableCell className="text-right">
{formatNumberWithCommas(ua.requests)}
</TableCell>
<TableCell className="text-right">
<span
className={
(ua.successful_requests / ua.requests) * 100 > 95
? "text-green-600"
: (ua.successful_requests / ua.requests) * 100 > 90
? "text-yellow-600"
: "text-red-600"
}
>
{((ua.successful_requests / ua.requests) * 100).toFixed(1)}%
</span>
</TableCell>
<TableCell className="text-right">
{formatNumberWithCommas(ua.tokens)}
</TableCell>
<TableCell className="text-right">
${formatNumberWithCommas(ua.spend, 2)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
<TabGroup>
<TabList className="mb-6">
<Tab>DAU/WAU/MAU</Tab>
<Tab>Per User Usage</Tab>
</TabList>
<TabPanels>
{/* DAU/WAU/MAU Tab Panel */}
<TabPanel>
<div className="mb-6">
<Title>DAU, WAU & MAU per Agent</Title>
<Subtitle>Active users across different time periods</Subtitle>
</div>
{/* Daily Activity Table */}
<Card>
<div className="flex justify-between items-center mb-4">
<Title>Daily User Agent Activity</Title>
<div className="flex items-center space-x-2">
<Button
variant="secondary"
size="xs"
onClick={handlePrevPage}
disabled={currentPage <= 1 || loading}
>
Previous
</Button>
<Text className="text-sm">
Page {currentPage} of {analyticsData.total_pages}
</Text>
<Button
variant="secondary"
size="xs"
onClick={handleNextPage}
disabled={currentPage >= analyticsData.total_pages || loading}
>
Next
</Button>
</div>
</div>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Date</TableHeaderCell>
<TableHeaderCell>User Agent</TableHeaderCell>
<TableHeaderCell className="text-right">DAU</TableHeaderCell>
<TableHeaderCell className="text-right">WAU</TableHeaderCell>
<TableHeaderCell className="text-right">MAU</TableHeaderCell>
<TableHeaderCell className="text-right">Requests</TableHeaderCell>
<TableHeaderCell className="text-right">Success Rate</TableHeaderCell>
<TableHeaderCell className="text-right">Tokens</TableHeaderCell>
<TableHeaderCell className="text-right">Spend</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{analyticsData.results.map((item, index) => (
<TableRow key={index}>
<TableCell>{item.date}</TableCell>
<TableCell className="font-medium">
{item.user_agent || "Unknown"}
</TableCell>
<TableCell className="text-right">
{formatNumberWithCommas(item.metrics.dau)}
</TableCell>
<TableCell className="text-right">
{formatNumberWithCommas(item.metrics.wau)}
</TableCell>
<TableCell className="text-right">
{formatNumberWithCommas(item.metrics.mau)}
</TableCell>
<TableCell className="text-right">
<div>
<div>{formatNumberWithCommas(item.metrics.total_requests)}</div>
<div className="text-xs text-gray-500">
{formatNumberWithCommas(item.metrics.successful_requests)} success
<TabGroup>
<TabList className="mb-6">
<Tab>DAU</Tab>
<Tab>WAU</Tab>
<Tab>MAU</Tab>
</TabList>
<TabPanels>
<TabPanel>
<div className="mb-4">
<Title className="text-lg">Daily Active Users - Last 7 Days</Title>
</div>
</div>
</TableCell>
<TableCell className="text-right">
<span
className={
(item.metrics.successful_requests / item.metrics.total_requests) * 100 > 95
? "text-green-600"
: (item.metrics.successful_requests / item.metrics.total_requests) * 100 > 90
? "text-yellow-600"
: "text-red-600"
}
>
{item.metrics.total_requests > 0
? ((item.metrics.successful_requests / item.metrics.total_requests) * 100).toFixed(1)
: "0"}%
</span>
</TableCell>
<TableCell className="text-right">
<div>
<div>{formatNumberWithCommas(item.metrics.total_tokens)}</div>
<div className="text-xs text-gray-500">
{formatNumberWithCommas(item.metrics.completed_tokens)} completed
<BarChart
data={dailyChartData}
index="date"
categories={uniqueUserAgents.slice(0, 3)}
colors={["blue", "green", "orange"]}
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
yAxisWidth={60}
showLegend={true}
/>
</TabPanel>
<TabPanel>
<div className="mb-4">
<Title className="text-lg">Weekly Active Users - Last 4 Weeks</Title>
</div>
</div>
</TableCell>
<TableCell className="text-right">
${formatNumberWithCommas(item.metrics.spend, 4)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{analyticsData.results.length === 0 && !loading && (
<div className="text-center py-8">
<Text>No user agent activity data found for the selected period.</Text>
</div>
)}
{loading && (
<div className="text-center py-8">
<Text>Loading...</Text>
</div>
)}
<BarChart
data={weeklyChartData}
index="week"
categories={uniqueUserAgents.slice(0, 3)}
colors={["blue", "green", "orange"]}
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
yAxisWidth={60}
showLegend={true}
/>
</TabPanel>
<TabPanel>
<div className="mb-4">
<Title className="text-lg">Monthly Active Users - Last 7 Months</Title>
</div>
<BarChart
data={monthlyChartData}
index="month"
categories={uniqueUserAgents.slice(0, 3)}
colors={["blue", "green", "orange"]}
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
yAxisWidth={60}
showLegend={true}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</TabPanel>
{/* Per User Usage Tab Panel */}
<TabPanel>
<PerUserUsage
accessToken={accessToken}
dateValue={dateValue}
formatAbbreviatedNumber={formatAbbreviatedNumber}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</Card>
</div>
);