diff --git a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py index f24997d96e5..994247ea943 100644 --- a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py +++ b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py @@ -169,23 +169,6 @@ async def _calculate_dau_wau_mau( return result -def _extract_user_agent_from_tag(tag: str) -> Optional[str]: - """ - Extract user agent name from tag. - Tags are in format "User-Agent: " or "User-Agent: /" - """ - if not tag.startswith("User-Agent: "): - return None - - user_agent = tag[12:] # Remove "User-Agent: " prefix - - # If it contains a version, extract just the name part - if "/" in user_agent: - return user_agent.split("/")[0] - - return user_agent - - @router.get( "/tag/user-agent/analytics", response_model=UserAgentAnalyticsResponse, @@ -203,7 +186,7 @@ async def get_user_agent_analytics( ), user_agent_filter: Optional[str] = Query( default=None, - description="Filter by specific user agent (e.g., 'curl', 'litellm')", + description="Filter by specific user agent tag", ), page: int = Query(default=1, description="Page number for pagination", ge=1), page_size: int = Query( @@ -212,20 +195,20 @@ async def get_user_agent_analytics( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ - Get user agent analytics including DAU, WAU, MAU, successful requests, and completed tokens by user agent tags. + Get user agent analytics including DAU, WAU, MAU, successful requests, and completed tokens by tags. - This endpoint analyzes the user-agent tags that are automatically tracked by the system - and provides analytics broken down by user agent. + This endpoint analyzes all tags that are tracked by the system and provides analytics + broken down by individual tags. Args: start_date: Start date for the analytics period (YYYY-MM-DD) end_date: End date for the analytics period (YYYY-MM-DD) - user_agent_filter: Filter results to specific user agent name + user_agent_filter: Filter results to specific tag page: Page number for pagination page_size: Number of items per page Returns: - UserAgentAnalyticsResponse: Analytics data broken down by user agent and date + UserAgentAnalyticsResponse: Analytics data broken down by tag and date """ from litellm.proxy.proxy_server import prisma_client @@ -242,25 +225,19 @@ async def get_user_agent_analytics( ) 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}, - }, + # Get all tags from the database + where_clause = {"date": {"gte": start_date, "lte": end_date}} + if user_agent_filter: + where_clause["tag"] = {"contains": user_agent_filter} + + tag_records = await prisma_client.db.litellm_dailytagspend.find_many( + where=where_clause, distinct=["tag"], ) - user_agent_tags = [record.tag for record in user_agent_tags_records] + tags = [record.tag for record in tag_records] - # Filter by user agent if specified - if user_agent_filter: - user_agent_tags = [ - tag for tag in user_agent_tags - if user_agent_filter.lower() in tag.lower() - ] - - if not user_agent_tags: + if not tags: return UserAgentAnalyticsResponse( results=[], total_count=0, @@ -269,12 +246,12 @@ async def get_user_agent_analytics( total_pages=0, ) - # Get daily activity data for user-agent tags + # Get daily activity data for tags daily_activity_response = await get_daily_activity( prisma_client=prisma_client, table_name="litellm_dailytagspend", entity_id_field="tag", - entity_id=user_agent_tags, + entity_id=tags, entity_metadata_field=None, start_date=start_date, end_date=end_date, @@ -284,7 +261,7 @@ async def get_user_agent_analytics( page_size=10000, # Large page size to get all data ) - # Process the results to calculate DAU/WAU/MAU and organize by user agent + # Process the results to calculate DAU/WAU/MAU and organize by tag results = [] daily_data_by_tag_and_date: Dict[str, Dict[str, DailySpendData]] = {} @@ -294,19 +271,16 @@ async def get_user_agent_analytics( # Get tag from breakdown data for tag, tag_metrics in daily_data.breakdown.entities.items(): - if tag.startswith("User-Agent: "): - if tag not in daily_data_by_tag_and_date: - daily_data_by_tag_and_date[tag] = {} - daily_data_by_tag_and_date[tag][date_str] = daily_data + if tag not in daily_data_by_tag_and_date: + daily_data_by_tag_and_date[tag] = {} + daily_data_by_tag_and_date[tag][date_str] = daily_data # Calculate DAU/WAU/MAU for each date and tag combination unique_dates: set[str] = set() for tag_data in daily_data_by_tag_and_date.values(): unique_dates.update(tag_data.keys()) - for tag in user_agent_tags: - user_agent = _extract_user_agent_from_tag(tag) - + for tag in tags: for date_str in sorted(unique_dates): if tag in daily_data_by_tag_and_date and date_str in daily_data_by_tag_and_date[tag]: daily_data = daily_data_by_tag_and_date[tag][date_str] @@ -334,13 +308,13 @@ async def get_user_agent_analytics( UserAgentActivityData( date=date_str, tag=tag, - user_agent=user_agent, + user_agent=tag, # Use the full tag as user_agent metrics=metrics, ) ) - # Sort results by date (most recent first) and then by user agent - results.sort(key=lambda x: (x.date, x.user_agent or ""), reverse=True) + # Sort results by date (most recent first) and then by tag + results.sort(key=lambda x: (x.date, x.tag), reverse=True) # Apply pagination total_count = len(results) @@ -381,9 +355,9 @@ async def get_user_agent_summary( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ - Get summary statistics for user agent activity. + Get summary statistics for tag activity. - Returns aggregated metrics across all user agents for the specified time period. + Returns aggregated metrics across all tags for the specified time period. """ from litellm.proxy.proxy_server import prisma_client @@ -400,26 +374,25 @@ async def get_user_agent_summary( ) try: - # Get all user-agent tags - user_agent_tags_records = await prisma_client.db.litellm_dailytagspend.find_many( + # Get all tags + tag_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] + tags = [record.tag for record in tag_records] - if not user_agent_tags: + if not tags: return { - "total_user_agents": 0, + "total_tags": 0, "total_requests": 0, "total_successful_requests": 0, "total_failed_requests": 0, "total_tokens": 0, "total_spend": 0.0, - "top_user_agents": [], + "top_tags": [], } # Get aggregated data @@ -427,7 +400,7 @@ async def get_user_agent_summary( prisma_client=prisma_client, table_name="litellm_dailytagspend", entity_id_field="tag", - entity_id=user_agent_tags, + entity_id=tags, entity_metadata_field=None, start_date=start_date, end_date=end_date, @@ -437,57 +410,54 @@ async def get_user_agent_summary( page_size=10000, ) - # Aggregate metrics by user agent - user_agent_totals: Dict[str, UserAgentMetrics] = {} + # Aggregate metrics by tag + tag_totals: Dict[str, UserAgentMetrics] = {} for daily_data in daily_activity_response.results: for tag, tag_metrics in daily_data.breakdown.entities.items(): - if tag.startswith("User-Agent: "): - user_agent = _extract_user_agent_from_tag(tag) - if user_agent is not None and user_agent not in user_agent_totals: - user_agent_totals[user_agent] = UserAgentMetrics() - - if user_agent is not None: - totals = user_agent_totals[user_agent] - totals.successful_requests += tag_metrics.metrics.successful_requests - totals.failed_requests += tag_metrics.metrics.failed_requests - totals.total_requests += tag_metrics.metrics.api_requests - totals.completed_tokens += tag_metrics.metrics.completion_tokens - totals.total_tokens += tag_metrics.metrics.total_tokens - totals.spend += tag_metrics.metrics.spend + if tag not in tag_totals: + tag_totals[tag] = UserAgentMetrics() + + totals = tag_totals[tag] + totals.successful_requests += tag_metrics.metrics.successful_requests + totals.failed_requests += tag_metrics.metrics.failed_requests + totals.total_requests += tag_metrics.metrics.api_requests + totals.completed_tokens += tag_metrics.metrics.completion_tokens + totals.total_tokens += tag_metrics.metrics.total_tokens + totals.spend += tag_metrics.metrics.spend # Calculate summary statistics - total_requests = sum(ua.total_requests for ua in user_agent_totals.values()) - total_successful_requests = sum(ua.successful_requests for ua in user_agent_totals.values()) - total_failed_requests = sum(ua.failed_requests for ua in user_agent_totals.values()) - total_tokens = sum(ua.total_tokens for ua in user_agent_totals.values()) - total_spend = sum(ua.spend for ua in user_agent_totals.values()) + total_requests = sum(tag.total_requests for tag in tag_totals.values()) + total_successful_requests = sum(tag.successful_requests for tag in tag_totals.values()) + total_failed_requests = sum(tag.failed_requests for tag in tag_totals.values()) + total_tokens = sum(tag.total_tokens for tag in tag_totals.values()) + total_spend = sum(tag.spend for tag in tag_totals.values()) - # Get top user agents by request count - top_user_agents = sorted( + # Get top tags by request count + top_tags = sorted( [ { - "user_agent": ua, + "tag": tag, "requests": metrics.total_requests, "successful_requests": metrics.successful_requests, "failed_requests": metrics.failed_requests, "tokens": metrics.total_tokens, "spend": metrics.spend, } - for ua, metrics in user_agent_totals.items() + for tag, metrics in tag_totals.items() ], key=lambda x: cast(int, x["requests"]), reverse=True, )[:10] # Top 10 return { - "total_user_agents": len(user_agent_totals), + "total_tags": len(tag_totals), "total_requests": total_requests, "total_successful_requests": total_successful_requests, "total_failed_requests": total_failed_requests, "total_tokens": total_tokens, "total_spend": total_spend, - "top_user_agents": top_user_agents, + "top_tags": top_tags, } except Exception as e: @@ -522,7 +492,7 @@ async def get_per_user_analytics( 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. + tag activity during the specified time period. Args: start_date: Start date for the analytics period (YYYY-MM-DD) @@ -548,30 +518,9 @@ async def get_per_user_analytics( ) 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 + # Get all tag records 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} } ) @@ -618,18 +567,18 @@ async def get_per_user_analytics( 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) + tag = record.tag # Use the full tag as user_agent 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 + user_agent=tag ) 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 + # If tag is different, keep the first one or prioritize certain ones + if tag and not user_metrics[user_id].user_agent: + user_metrics[user_id].user_agent = tag # Aggregate metrics user_metrics[user_id].successful_requests += record.successful_requests or 0 diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index b2ea9150db9..3dda412e203 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -61,7 +61,7 @@ import { EntityList } from "./entity_usage" import { formatNumberWithCommas } from "@/utils/dataUtils" import { valueFormatterSpend } from "./usage/utils/value_formatters" import CloudZeroExportModal from "./cloudzero_export_modal" -import { UiLoadingSpinner } from "./ui/ui-loading-spinner" +import { ChartLoader } from "./shared/chart_loader" interface NewUsagePageProps { accessToken: string | null @@ -368,23 +368,6 @@ const NewUsagePage: React.FC = ({ accessToken, userRole, user return () => clearTimeout(timeoutId) }, [fetchUserSpendData]) - // Enhanced loading component with better visual feedback - const ChartLoader = () => ( -
-
- -
- - {isDateChanging ? "Processing date selection..." : "Loading chart data..."} - - - {isDateChanging ? "This will only take a moment" : "Fetching your data"} - -
-
-
- ) - const modelMetrics = processActivityData(userSpendData, "models") const keyMetrics = processActivityData(userSpendData, "api_keys") const mcpServerMetrics = processActivityData(userSpendData, "mcp_servers") @@ -530,7 +513,7 @@ const NewUsagePage: React.FC = ({ accessToken, userRole, user Daily Spend {loading ? ( - + ) : ( = ({ accessToken, userRole, user {loading ? ( - + ) : ( = ({ accessToken, userRole, user Spend by Provider {loading ? ( - + ) : ( diff --git a/ui/litellm-dashboard/src/components/shared/chart_loader.tsx b/ui/litellm-dashboard/src/components/shared/chart_loader.tsx new file mode 100644 index 00000000000..c6ab0c552be --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/chart_loader.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { UiLoadingSpinner } from "../ui/ui-loading-spinner"; + +interface ChartLoaderProps { + isDateChanging?: boolean; +} + +export const ChartLoader: React.FC = ({ isDateChanging = false }) => ( +
+
+ +
+ + {isDateChanging ? "Processing date selection..." : "Loading chart data..."} + + + {isDateChanging ? "This will only take a moment" : "Fetching your data"} + +
+
+
+); + +export default ChartLoader; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/user_agent_activity.tsx b/ui/litellm-dashboard/src/components/user_agent_activity.tsx index ec2d1dfb150..0d0eda36159 100644 --- a/ui/litellm-dashboard/src/components/user_agent_activity.tsx +++ b/ui/litellm-dashboard/src/components/user_agent_activity.tsx @@ -26,9 +26,10 @@ import { } from "@tremor/react"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { userAgentAnalyticsCall, userAgentSummaryCall } from "./networking"; -import UsageDatePicker from "./shared/usage_date_picker"; +import AdvancedDatePicker from "./shared/advanced_date_picker"; import PerUserUsage from "./per_user_usage"; import { DateRangePickerValue } from "@tremor/react"; +import { ChartLoader } from "./shared/chart_loader"; interface UserAgentMetrics { dau: number; @@ -58,14 +59,14 @@ interface UserAgentAnalyticsResponse { } interface UserAgentSummaryData { - total_user_agents: number; + total_tags: number; total_requests: number; total_successful_requests: number; total_failed_requests: number; total_tokens: number; total_spend: number; - top_user_agents: Array<{ - user_agent: string; + top_tags: Array<{ + tag: string; requests: number; successful_requests: number; failed_requests: number; @@ -92,13 +93,13 @@ const UserAgentActivity: React.FC = ({ }); const [summaryData, setSummaryData] = useState({ - total_user_agents: 0, + total_tags: 0, total_requests: 0, total_successful_requests: 0, total_failed_requests: 0, total_tokens: 0, total_spend: 0, - top_user_agents: [], + top_tags: [], }); const [dateValue, setDateValue] = useState({ @@ -107,50 +108,84 @@ const UserAgentActivity: React.FC = ({ }); const [userAgentFilter, setUserAgentFilter] = useState(""); - const [loading, setLoading] = useState(false); + const [analyticsLoading, setAnalyticsLoading] = useState(false); + const [summaryLoading, setSummaryLoading] = useState(false); + const [isDateChanging, setIsDateChanging] = useState(false); const [currentPage, setCurrentPage] = useState(1); - const fetchData = async () => { + const fetchAnalyticsData = async () => { if (!accessToken || !dateValue.from || !dateValue.to) return; - setLoading(true); + setAnalyticsLoading(true); try { - const [analytics, summary] = await Promise.all([ - userAgentAnalyticsCall( - accessToken, - dateValue.from, - dateValue.to, - currentPage, - 50, - userAgentFilter || undefined - ), - userAgentSummaryCall(accessToken, dateValue.from, dateValue.to), - ]); + const analytics = await userAgentAnalyticsCall( + accessToken, + dateValue.from, + dateValue.to, + currentPage, + 50, + userAgentFilter || undefined + ); setAnalyticsData(analytics); + } catch (error) { + console.error("Failed to fetch user agent analytics data:", error); + } finally { + setAnalyticsLoading(false); + setIsDateChanging(false); + } + }; + + const fetchSummaryData = async () => { + if (!accessToken || !dateValue.from || !dateValue.to) return; + + setSummaryLoading(true); + try { + const summary = await userAgentSummaryCall(accessToken, dateValue.from, dateValue.to); setSummaryData(summary); } catch (error) { - console.error("Failed to fetch user agent data:", error); + console.error("Failed to fetch user agent summary data:", error); } finally { - setLoading(false); + setSummaryLoading(false); + setIsDateChanging(false); } }; + // Super responsive date change handler + const handleDateChange = (newValue: DateRangePickerValue) => { + // Instant visual feedback + setIsDateChanging(true); + setAnalyticsLoading(true); + setSummaryLoading(true); + + // Update date immediately for UI responsiveness + setDateValue(newValue); + setCurrentPage(1); // Reset to first page when date changes + }; + + // Debounced effect for data fetching useEffect(() => { - fetchData(); - }, [accessToken, dateValue, userAgentFilter, currentPage]); + if (!dateValue.from || !dateValue.to) return; - const handleNextPage = () => { - if (currentPage < analyticsData.total_pages) { - setCurrentPage(currentPage + 1); - } - }; + const timeoutId = setTimeout(() => { + // Call both fetch functions independently + fetchAnalyticsData(); + fetchSummaryData(); + }, 50); // Very short debounce - const handlePrevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; + return () => clearTimeout(timeoutId); + }, [accessToken, dateValue, userAgentFilter]); + + // Separate effect for pagination that only affects analytics + useEffect(() => { + if (!dateValue.from || !dateValue.to) return; + + const timeoutId = setTimeout(() => { + fetchAnalyticsData(); + }, 50); + + return () => clearTimeout(timeoutId); + }, [currentPage]); // Aggregate data by user agent for charts const aggregatedByUserAgent = analyticsData.results.reduce((acc, item) => { @@ -184,10 +219,26 @@ const UserAgentActivity: React.FC = ({ (a: any, b: any) => b.total_requests - a.total_requests ); - const successRateData = summaryData.top_user_agents.map((ua) => ({ - user_agent: ua.user_agent, - success_rate: ua.successful_requests / (ua.requests || 1) * 100, - total_requests: ua.requests, + // Helper function to extract user agent from tag + const extractUserAgent = (tag: string): string => { + if (tag.startsWith("User-Agent: ")) { + return tag.replace("User-Agent: ", ""); + } + return tag; + }; + + // Helper function to truncate user agent name with tooltip + const truncateUserAgent = (userAgent: string): string => { + if (userAgent.length > 10) { + return userAgent.substring(0, 10) + "..."; + } + return userAgent; + }; + + const successRateData = (summaryData.top_tags || []).map((tag) => ({ + user_agent: extractUserAgent(tag.tag), + success_rate: tag.successful_requests / (tag.requests || 1) * 100, + total_requests: tag.requests, })); // Get unique user agents for chart @@ -263,12 +314,9 @@ const UserAgentActivity: React.FC = ({ {/* Date Range Picker */} - { - setDateValue(value); - setCurrentPage(1); // Reset to first page when date changes - }} + onValueChange={handleDateChange} /> @@ -289,49 +337,59 @@ const UserAgentActivity: React.FC = ({ {/* Top 4 User Agents Cards */} - - {summaryData.top_user_agents.slice(0, 4).map((ua, index) => ( - - - {ua.user_agent} - -
-
- Success Requests - {formatAbbreviatedNumber(ua.successful_requests)} -
-
- Total Tokens - {formatAbbreviatedNumber(ua.tokens)} -
-
- Total Cost - ${formatAbbreviatedNumber(ua.spend, 4)} -
-
+ {summaryLoading ? ( + + - ))} - {/* Fill remaining slots if less than 4 agents */} - {Array.from({ length: Math.max(0, 4 - summaryData.top_user_agents.length) }).map((_, index) => ( - - No Data -
-
- Success Requests - - + ) : ( + + {(summaryData.top_tags || []).slice(0, 4).map((tag, index) => { + const userAgent = extractUserAgent(tag.tag); + const displayName = truncateUserAgent(userAgent); + return ( + + + {displayName} + +
+
+ Success Requests + {formatAbbreviatedNumber(tag.successful_requests)} +
+
+ Total Tokens + {formatAbbreviatedNumber(tag.tokens)} +
+
+ Total Cost + ${formatAbbreviatedNumber(tag.spend, 4)} +
+
+
+ ); + })} + {/* Fill remaining slots if less than 4 agents */} + {Array.from({ length: Math.max(0, 4 - (summaryData.top_tags || []).length) }).map((_, index) => ( + + No Data +
+
+ Success Requests + - +
+
+ Total Tokens + - +
+
+ Total Cost + - +
-
- Total Tokens - - -
-
- Total Cost - - -
-
- - ))} - + + ))} + + )} {/* Main TabGroup for DAU/WAU/MAU vs Per User Usage */} @@ -361,45 +419,57 @@ const UserAgentActivity: React.FC = ({
Daily Active Users - Last 7 Days
- formatAbbreviatedNumber(value)} - yAxisWidth={60} - showLegend={true} - /> + {analyticsLoading ? ( + + ) : ( + formatAbbreviatedNumber(value)} + yAxisWidth={60} + showLegend={true} + /> + )}
Weekly Active Users - Last 4 Weeks
- formatAbbreviatedNumber(value)} - yAxisWidth={60} - showLegend={true} - /> + {analyticsLoading ? ( + + ) : ( + formatAbbreviatedNumber(value)} + yAxisWidth={60} + showLegend={true} + /> + )}
Monthly Active Users - Last 7 Months
- formatAbbreviatedNumber(value)} - yAxisWidth={60} - showLegend={true} - /> + {analyticsLoading ? ( + + ) : ( + formatAbbreviatedNumber(value)} + yAxisWidth={60} + showLegend={true} + /> + )}