mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-21 08:26:34 +00:00
[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:
co-authored by
Cursor Agent
ishaan
parent
eb8a338d9b
commit
92cdac2e87
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user