From 4a85a91f206b363d2cf7f038f16bb54ec3b8580d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 1 Aug 2025 15:29:13 -0700 Subject: [PATCH] [QA] Viewing Agent Activity Headers on UI Usage Page (#13212) * qa - agents * refactored WAU, MAU and DAU endpoints * fixes for dau, wau, mau * use stack=true * fixes for DAU calc * fixes for rendering WAU, MAU * use 1 section for topline * Fixes for endpoint * remove filter * fix spacing * fix activity * working UI rendering * fixes for chart data * allow selecting specific tags * add DistinctTagResponse endpoints * use wide selector * add types * fixes for UI rendering * get_per_user_analytics --- .../user_agent_analytics_endpoints.py | 939 ++++++++++-------- .../src/components/networking.tsx | 258 ++++- .../src/components/per_user_usage.tsx | 13 +- .../src/components/user_agent_activity.tsx | 632 +++++++----- 4 files changed, 1152 insertions(+), 690 deletions(-) diff --git a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py index 994247ea943..55263dcd6bf 100644 --- a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py +++ b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py @@ -1,62 +1,73 @@ """ User Agent Analytics Endpoints -This module provides endpoints for tracking user agent activity metrics including: -- Daily Active Users (DAU) by tags -- Weekly Active Users (WAU) by tags -- Monthly Active Users (MAU) by tags -- Successful requests by tags -- Completed tokens by tags +This module provides optimized endpoints for tracking user agent activity metrics including: +- Daily Active Users (DAU) by tags for configurable number of days +- Weekly Active Users (WAU) by tags for configurable number of weeks +- Monthly Active Users (MAU) by tags for configurable number of months +- Summary analytics by tags -These endpoints extend the existing tag daily activity functionality to provide -user agent specific analytics by using the user-agent tags that are automatically -tracked by the system. +These endpoints use optimized single SQL queries with joins to efficiently calculate +user metrics from tag activity data and return time series for dashboard visualization. """ from datetime import datetime, timedelta -from typing import Dict, List, Optional, Set, cast +from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel 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 get_daily_activity -from litellm.types.proxy.management_endpoints.common_daily_activity import ( - DailySpendData, -) + +# Constants for analytics periods +MAX_DAYS = 7 # Number of days to show in DAU analytics +MAX_WEEKS = 7 # Number of weeks to show in WAU analytics +MAX_MONTHS = 7 # Number of months to show in MAU analytics +MAX_TAGS = 250 # Maximum number of distinct tags to return router = APIRouter() -class UserAgentMetrics(BaseModel): - """Metrics for user agent activity""" - dau: int = 0 # Daily Active Users - wau: int = 0 # Weekly Active Users - mau: int = 0 # Monthly Active Users - successful_requests: int = 0 - failed_requests: int = 0 - total_requests: int = 0 - completed_tokens: int = 0 - total_tokens: int = 0 - spend: float = 0.0 - - -class UserAgentActivityData(BaseModel): - """User agent activity data for a specific date""" - date: str +class TagActiveUsersResponse(BaseModel): + """Response for tag active users metrics""" tag: str - user_agent: Optional[str] = None - metrics: UserAgentMetrics + active_users: int + date: str # The specific date or period identifier + period_start: Optional[str] = None # For WAU/MAU, this will be the start of the period + period_end: Optional[str] = None # For WAU/MAU, this will be the end of the period -class UserAgentAnalyticsResponse(BaseModel): - """Response for user agent analytics""" - results: List[UserAgentActivityData] - total_count: int - page: int - page_size: int - total_pages: int +class ActiveUsersAnalyticsResponse(BaseModel): + """Response for active users analytics""" + results: List[TagActiveUsersResponse] + + +class TagSummaryMetrics(BaseModel): + """Summary metrics for a tag""" + tag: str + unique_users: int + total_requests: int + successful_requests: int + failed_requests: int + total_tokens: int + total_spend: float + + +class TagSummaryResponse(BaseModel): + """Response for tag summary analytics""" + results: List[TagSummaryMetrics] + + +class DistinctTagResponse(BaseModel): + """Response for distinct user agent tags""" + tag: str + + +class DistinctTagsResponse(BaseModel): + """Response for all distinct user agent tags""" + results: List[DistinctTagResponse] + class PerUserMetrics(BaseModel): @@ -80,135 +91,430 @@ class PerUserAnalyticsResponse(BaseModel): total_pages: int -async def _get_unique_users_for_tags( - prisma_client, - tags: List[str], - start_date: str, - end_date: str, -) -> Dict[str, Set[str]]: - """ - Get unique users for each tag by looking up api_key -> user_id mappings - """ - from litellm.proxy.proxy_server import prisma_client as db_client - - if not db_client: - return {} - - # Get all records for the specified tags and date range - tag_records = await db_client.db.litellm_dailytagspend.find_many( - where={ - "tag": {"in": 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 {} - - # Lookup user_id for each api_key - api_key_records = await db_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 - } - - # Group unique users by tag - tag_users: Dict[str, Set[str]] = {} - 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] - tag = record.tag - if tag not in tag_users: - tag_users[tag] = set() - tag_users[tag].add(user_id) - - return tag_users - - -async def _calculate_dau_wau_mau( - prisma_client, - tags: List[str], - target_date: str, -) -> Dict[str, Dict[str, int]]: - """ - Calculate DAU, WAU, MAU for given tags and date - """ - target_dt = datetime.strptime(target_date, "%Y-%m-%d") - - # Calculate date ranges - dau_start = target_date - dau_end = target_date - - wau_start = (target_dt - timedelta(days=6)).strftime("%Y-%m-%d") - wau_end = target_date - - mau_start = (target_dt - timedelta(days=29)).strftime("%Y-%m-%d") - mau_end = target_date - - # Get unique users for each period - dau_users = await _get_unique_users_for_tags(prisma_client, tags, dau_start, dau_end) - wau_users = await _get_unique_users_for_tags(prisma_client, tags, wau_start, wau_end) - mau_users = await _get_unique_users_for_tags(prisma_client, tags, mau_start, mau_end) - - result = {} - for tag in tags: - result[tag] = { - "dau": len(dau_users.get(tag, set())), - "wau": len(wau_users.get(tag, set())), - "mau": len(mau_users.get(tag, set())), - } - - return result - - @router.get( - "/tag/user-agent/analytics", - response_model=UserAgentAnalyticsResponse, + "/tag/distinct", + response_model=DistinctTagsResponse, tags=["tag management", "user agent analytics"], dependencies=[Depends(user_api_key_auth)], ) -async def get_user_agent_analytics( - start_date: Optional[str] = Query( +async def get_distinct_user_agent_tags( + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get all distinct user agent tags up to a maximum of {MAX_TAGS} tags. + + This endpoint returns all unique user agent tags found in the database, + sorted by frequency of usage. + + Returns: + DistinctTagsResponse: List of distinct user agent tags + """ + 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}, + ) + + try: + sql_query = f""" + SELECT + dts.tag, + COUNT(*) as usage_count + FROM "LiteLLM_DailyTagSpend" dts + WHERE dts.tag LIKE 'User-Agent:%' OR dts.tag NOT LIKE '%:%' + GROUP BY dts.tag + ORDER BY usage_count DESC + LIMIT {MAX_TAGS} + """ + + db_response = await prisma_client.db.query_raw(sql_query) + + results = [ + DistinctTagResponse(tag=row["tag"]) + for row in db_response + ] + + return DistinctTagsResponse(results=results) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to fetch distinct user agent tags: {str(e)}", + ) + + +@router.get( + "/tag/dau", + response_model=ActiveUsersAnalyticsResponse, + tags=["tag management", "user agent analytics"], + dependencies=[Depends(user_api_key_auth)], +) +async def get_daily_active_users( + tag_filter: Optional[str] = Query( default=None, - description="Start date in YYYY-MM-DD format", + description="Filter by specific tag (optional)", ), - end_date: Optional[str] = Query( + tag_filters: Optional[List[str]] = Query( default=None, - description="End date in YYYY-MM-DD format", - ), - user_agent_filter: Optional[str] = Query( - default=None, - description="Filter by specific user agent tag", - ), - 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 + description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", ), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ - Get user agent analytics including DAU, WAU, MAU, successful requests, and completed tokens by tags. + Get Daily Active Users (DAU) by tags for the last {MAX_DAYS} days ending on UTC today + 1 day. - This endpoint analyzes all tags that are tracked by the system and provides analytics - broken down by individual tags. + This endpoint efficiently calculates unique users per tag for each of the last {MAX_DAYS} days + using a single optimized SQL query, perfect for dashboard time series visualization. + + Args: + tag_filter: Optional filter to specific tag (legacy) + tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) + + Returns: + ActiveUsersAnalyticsResponse: DAU data by tag for each of the last {MAX_DAYS} days + """ + 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}, + ) + + try: + # Calculate end_date as UTC today + 1 day + from datetime import timezone + end_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) + end_date = end_dt.strftime("%Y-%m-%d") + + # Calculate date range (last MAX_DAYS days) + start_dt = end_dt - timedelta(days=MAX_DAYS) + start_date = start_dt.strftime("%Y-%m-%d") + + # Build SQL query with optional tag filter(s) + where_clause = "WHERE dts.date >= $1 AND dts.date <= $2 AND vt.user_id IS NOT NULL" + params = [start_date, end_date] + + # Handle multiple tag filters (takes precedence over single tag filter) + if tag_filters and len(tag_filters) > 0: + tag_conditions = [] + for i, tag in enumerate(tag_filters): + param_index = len(params) + 1 + tag_conditions.append(f"dts.tag = ${param_index}") + params.append(tag) + where_clause += f" AND ({' OR '.join(tag_conditions)})" + elif tag_filter: + where_clause += " AND dts.tag ILIKE $3" + params.append(f"%{tag_filter}%") + + sql_query = f""" + SELECT + dts.tag, + dts.date, + COUNT(DISTINCT vt.user_id) as active_users + FROM "LiteLLM_DailyTagSpend" dts + INNER JOIN "LiteLLM_VerificationToken" vt ON dts.api_key = vt.token + {where_clause} + GROUP BY dts.tag, dts.date + ORDER BY dts.date DESC, active_users DESC + """ + + db_response = await prisma_client.db.query_raw(sql_query, *params) + + results = [ + TagActiveUsersResponse( + tag=row["tag"], + active_users=row["active_users"], + date=row["date"] + ) + for row in db_response + ] + + return ActiveUsersAnalyticsResponse(results=results) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to fetch DAU analytics: {str(e)}", + ) + + +@router.get( + "/tag/wau", + response_model=ActiveUsersAnalyticsResponse, + tags=["tag management", "user agent analytics"], + dependencies=[Depends(user_api_key_auth)], +) +async def get_weekly_active_users( + tag_filter: Optional[str] = Query( + default=None, + description="Filter by specific tag (optional)", + ), + tag_filters: Optional[List[str]] = Query( + default=None, + description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get Weekly Active Users (WAU) by tags for the last {MAX_WEEKS} weeks ending on UTC today + 1 day. + + Shows week-by-week breakdown: + - Week 1 (Jan 1): Earliest week (7 weeks ago) + - Week 2 (Jan 8): Next week (6 weeks ago) + - Week 3 (Jan 15): Next week (5 weeks ago) + - ... and so on for {MAX_WEEKS} weeks total + - Week 7: Most recent week ending on UTC today + 1 day + + Args: + tag_filter: Optional filter to specific tag (legacy) + tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) + + Returns: + ActiveUsersAnalyticsResponse: WAU data by tag for each of the last {MAX_WEEKS} weeks with descriptive week labels (e.g., "Week 1 (Jan 1)") + """ + 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}, + ) + + try: + # Calculate end_date as UTC today + 1 day + from datetime import timezone + end_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) + end_date = end_dt.strftime("%Y-%m-%d") + + # Calculate date range for all weeks (49 days total) + # Start from 48 days before end_date to cover exactly MAX_WEEKS complete weeks + start_dt = end_dt - timedelta(days=(MAX_WEEKS * 7 - 1)) # MAX_WEEKS weeks * 7 days - 1 + start_date = start_dt.strftime("%Y-%m-%d") + + # Build SQL query with optional tag filter(s) + where_clause = "WHERE dts.date >= $1 AND dts.date <= $2 AND vt.user_id IS NOT NULL" + params = [start_date, end_date] + + # Handle multiple tag filters (takes precedence over single tag filter) + if tag_filters and len(tag_filters) > 0: + tag_conditions = [] + for i, tag in enumerate(tag_filters): + param_index = len(params) + 1 + tag_conditions.append(f"dts.tag = ${param_index}") + params.append(tag) + where_clause += f" AND ({' OR '.join(tag_conditions)})" + elif tag_filter: + where_clause += " AND dts.tag ILIKE $3" + params.append(f"%{tag_filter}%") + + # Use window function to group by weeks with clear week numbering + sql_query = f""" + WITH weekly_data AS ( + SELECT + dts.tag, + dts.date, + vt.user_id, + -- Calculate week number (0 = Week 1 most recent, 1 = Week 2, etc.) + FLOOR((DATE '{end_date}' - dts.date::date) / 7) as week_offset + FROM "LiteLLM_DailyTagSpend" dts + INNER JOIN "LiteLLM_VerificationToken" vt ON dts.api_key = vt.token + {where_clause} + ) + SELECT + tag, + COUNT(DISTINCT user_id) as active_users, + -- Week identifier with month and day (Week 1 (earliest), Week 2, etc.) + 'Week ' || ({MAX_WEEKS} - week_offset)::text || ' (' || + TO_CHAR(DATE '{end_date}' - (week_offset * 7 || ' days')::interval - '6 days'::interval, 'Mon DD') || ')' as date, + -- Calculate week start and end dates for each week + (DATE '{end_date}' - (week_offset * 7 || ' days')::interval - '6 days'::interval)::text as period_start, + (DATE '{end_date}' - (week_offset * 7 || ' days')::interval)::text as period_end, + week_offset + FROM weekly_data + WHERE week_offset < {MAX_WEEKS} + GROUP BY tag, week_offset + ORDER BY week_offset DESC, active_users DESC + """ + + db_response = await prisma_client.db.query_raw(sql_query, *params) + + results = [ + TagActiveUsersResponse( + tag=row["tag"], + active_users=row["active_users"], + date=row["date"], # This will be "Week 1 (Jan 15)", "Week 2 (Jan 8)", etc. + period_start=row["period_start"], + period_end=row["period_end"] + ) + for row in db_response + ] + + return ActiveUsersAnalyticsResponse(results=results) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to fetch WAU analytics: {str(e)}", + ) + + +@router.get( + "/tag/mau", + response_model=ActiveUsersAnalyticsResponse, + tags=["tag management", "user agent analytics"], + dependencies=[Depends(user_api_key_auth)], +) +async def get_monthly_active_users( + tag_filter: Optional[str] = Query( + default=None, + description="Filter by specific tag (optional)", + ), + tag_filters: Optional[List[str]] = Query( + default=None, + description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get Monthly Active Users (MAU) by tags for the last {MAX_MONTHS} months ending on UTC today + 1 day. + + Shows month-by-month breakdown: + - Month 1 (Nov): Earliest month (7 months ago, 30-day period) + - Month 2 (Dec): Next month (6 months ago) + - Month 3 (Jan): Next month (5 months ago) + - ... and so on for {MAX_MONTHS} months total + - Month 7: Most recent month ending on UTC today + 1 day + + Args: + tag_filter: Optional filter to specific tag (legacy) + tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) + + Returns: + ActiveUsersAnalyticsResponse: MAU data by tag for each of the last {MAX_MONTHS} months with descriptive month labels (e.g., "Month 1 (Nov)") + """ + 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}, + ) + + try: + # Calculate end_date as UTC today + 1 day + from datetime import timezone + end_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) + end_date = end_dt.strftime("%Y-%m-%d") + + # Calculate date range for all months (210 days total) + # Start from 209 days before end_date to cover exactly MAX_MONTHS complete months + start_dt = end_dt - timedelta(days=(MAX_MONTHS * 30 - 1)) # MAX_MONTHS months * 30 days - 1 + start_date = start_dt.strftime("%Y-%m-%d") + + # Build SQL query with optional tag filter(s) + where_clause = "WHERE dts.date >= $1 AND dts.date <= $2 AND vt.user_id IS NOT NULL" + params = [start_date, end_date] + + # Handle multiple tag filters (takes precedence over single tag filter) + if tag_filters and len(tag_filters) > 0: + tag_conditions = [] + for i, tag in enumerate(tag_filters): + param_index = len(params) + 1 + tag_conditions.append(f"dts.tag = ${param_index}") + params.append(tag) + where_clause += f" AND ({' OR '.join(tag_conditions)})" + elif tag_filter: + where_clause += " AND dts.tag ILIKE $3" + params.append(f"%{tag_filter}%") + + # Use window function to group by months (30-day periods) with clear month numbering + sql_query = f""" + WITH monthly_data AS ( + SELECT + dts.tag, + dts.date, + vt.user_id, + -- Calculate month number (0 = Month 1 most recent, 1 = Month 2, etc.) + FLOOR((DATE '{end_date}' - dts.date::date) / 30) as month_offset + FROM "LiteLLM_DailyTagSpend" dts + INNER JOIN "LiteLLM_VerificationToken" vt ON dts.api_key = vt.token + {where_clause} + ) + SELECT + tag, + COUNT(DISTINCT user_id) as active_users, + -- Month identifier with month name (Month 1 (earliest), Month 2, etc.) + 'Month ' || ({MAX_MONTHS} - month_offset)::text || ' (' || + TO_CHAR(DATE '{end_date}' - (month_offset * 30 || ' days')::interval - '29 days'::interval, 'Mon') || ')' as date, + -- Calculate month start and end dates for each month + (DATE '{end_date}' - (month_offset * 30 || ' days')::interval - '29 days'::interval)::text as period_start, + (DATE '{end_date}' - (month_offset * 30 || ' days')::interval)::text as period_end, + month_offset + FROM monthly_data + WHERE month_offset < {MAX_MONTHS} + GROUP BY tag, month_offset + ORDER BY month_offset DESC, active_users DESC + """ + + db_response = await prisma_client.db.query_raw(sql_query, *params) + + results = [ + TagActiveUsersResponse( + tag=row["tag"], + active_users=row["active_users"], + date=row["date"], # This will be "Month 1 (Jan)", "Month 2 (Dec)", etc. + period_start=row["period_start"], + period_end=row["period_end"] + ) + for row in db_response + ] + + return ActiveUsersAnalyticsResponse(results=results) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to fetch MAU analytics: {str(e)}", + ) + + +@router.get( + "/tag/summary", + response_model=TagSummaryResponse, + tags=["tag management", "user agent analytics"], + dependencies=[Depends(user_api_key_auth)], +) +async def get_tag_summary( + start_date: str = Query( + description="Start date in YYYY-MM-DD format" + ), + end_date: str = Query( + description="End date in YYYY-MM-DD format" + ), + tag_filter: Optional[str] = Query( + default=None, + description="Filter by specific tag (optional)", + ), + tag_filters: Optional[List[str]] = Query( + default=None, + description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get summary analytics for tags including unique users, requests, tokens, and spend. 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 tag - page: Page number for pagination - page_size: Number of items per page + tag_filter: Optional filter to specific tag (legacy) + tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) Returns: - UserAgentAnalyticsResponse: Analytics data broken down by tag and date + TagSummaryResponse: Summary analytics data by tag """ from litellm.proxy.proxy_server import prisma_client @@ -218,252 +524,69 @@ async def get_user_agent_analytics( 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 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"], - ) + # Validate date format + datetime.strptime(start_date, "%Y-%m-%d") + datetime.strptime(end_date, "%Y-%m-%d") - tags = [record.tag for record in tag_records] + # Build SQL query with optional tag filter(s) + where_clause = "WHERE dts.date >= $1 AND dts.date <= $2" + params = [start_date, end_date] - if not tags: - return UserAgentAnalyticsResponse( - results=[], - total_count=0, - page=page, - page_size=page_size, - total_pages=0, + # Handle multiple tag filters (takes precedence over single tag filter) + if tag_filters and len(tag_filters) > 0: + tag_conditions = [] + for i, tag in enumerate(tag_filters): + param_index = len(params) + 1 + tag_conditions.append(f"dts.tag = ${param_index}") + params.append(tag) + where_clause += f" AND ({' OR '.join(tag_conditions)})" + elif tag_filter: + where_clause += " AND dts.tag ILIKE $3" + params.append(f"%{tag_filter}%") + + sql_query = f""" + SELECT + dts.tag, + COUNT(DISTINCT vt.user_id) as unique_users, + SUM(dts.api_requests) as total_requests, + SUM(dts.successful_requests) as successful_requests, + SUM(dts.failed_requests) as failed_requests, + SUM(dts.prompt_tokens + dts.completion_tokens) as total_tokens, + SUM(dts.spend) as total_spend + FROM "LiteLLM_DailyTagSpend" dts + LEFT JOIN "LiteLLM_VerificationToken" vt ON dts.api_key = vt.token + {where_clause} + GROUP BY dts.tag + ORDER BY total_requests DESC + """ + + db_response = await prisma_client.db.query_raw(sql_query, *params) + + results = [ + TagSummaryMetrics( + tag=row["tag"], + unique_users=row["unique_users"] or 0, + total_requests=int(row["total_requests"] or 0), + successful_requests=int(row["successful_requests"] or 0), + failed_requests=int(row["failed_requests"] or 0), + total_tokens=int(row["total_tokens"] or 0), + total_spend=float(row["total_spend"] or 0.0) ) + for row in db_response + ] - # 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=tags, - entity_metadata_field=None, - start_date=start_date, - end_date=end_date, - model=None, - api_key=None, - page=1, # Get all data first, then paginate our results - page_size=10000, # Large page size to get all data - ) + return TagSummaryResponse(results=results) - # Process the results to calculate DAU/WAU/MAU and organize by tag - results = [] - daily_data_by_tag_and_date: Dict[str, Dict[str, DailySpendData]] = {} - - # Organize data by tag and date - for daily_data in daily_activity_response.results: - date_str = daily_data.date.strftime("%Y-%m-%d") - - # Get tag from breakdown data - for tag, tag_metrics in daily_data.breakdown.entities.items(): - 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 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] - tag_breakdown = daily_data.breakdown.entities.get(tag) - - if tag_breakdown: - # Calculate DAU/WAU/MAU for this specific date and tag - dau_wau_mau = await _calculate_dau_wau_mau( - prisma_client, [tag], date_str - ) - - metrics = UserAgentMetrics( - dau=dau_wau_mau.get(tag, {}).get("dau", 0), - wau=dau_wau_mau.get(tag, {}).get("wau", 0), - mau=dau_wau_mau.get(tag, {}).get("mau", 0), - successful_requests=tag_breakdown.metrics.successful_requests, - failed_requests=tag_breakdown.metrics.failed_requests, - total_requests=tag_breakdown.metrics.api_requests, - completed_tokens=tag_breakdown.metrics.completion_tokens, - total_tokens=tag_breakdown.metrics.total_tokens, - spend=tag_breakdown.metrics.spend, - ) - - results.append( - UserAgentActivityData( - date=date_str, - tag=tag, - user_agent=tag, # Use the full tag as user_agent - metrics=metrics, - ) - ) - - # 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) - 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 UserAgentAnalyticsResponse( - 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 user agent analytics: {str(e)}", - ) - - -@router.get( - "/tag/user-agent/summary", - tags=["tag management", "user agent analytics"], - dependencies=[Depends(user_api_key_auth)], -) -async def get_user_agent_summary( - 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", - ), - user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), -): - """ - Get summary statistics for tag activity. - - Returns aggregated metrics across all tags for the specified time period. - """ - 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: + except ValueError as e: raise HTTPException( status_code=400, - detail={"error": "Please provide start_date and end_date"}, + detail=f"Invalid date format. Use YYYY-MM-DD: {str(e)}", ) - - try: - # Get all tags - tag_records = await prisma_client.db.litellm_dailytagspend.find_many( - where={ - "date": {"gte": start_date, "lte": end_date}, - }, - distinct=["tag"], - ) - - tags = [record.tag for record in tag_records] - - if not tags: - return { - "total_tags": 0, - "total_requests": 0, - "total_successful_requests": 0, - "total_failed_requests": 0, - "total_tokens": 0, - "total_spend": 0.0, - "top_tags": [], - } - - # Get aggregated data - daily_activity_response = await get_daily_activity( - prisma_client=prisma_client, - table_name="litellm_dailytagspend", - entity_id_field="tag", - entity_id=tags, - entity_metadata_field=None, - start_date=start_date, - end_date=end_date, - model=None, - api_key=None, - page=1, - page_size=10000, - ) - - # 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 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(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 tags by request count - top_tags = sorted( - [ - { - "tag": tag, - "requests": metrics.total_requests, - "successful_requests": metrics.successful_requests, - "failed_requests": metrics.failed_requests, - "tokens": metrics.total_tokens, - "spend": metrics.spend, - } - for tag, metrics in tag_totals.items() - ], - key=lambda x: cast(int, x["requests"]), - reverse=True, - )[:10] # Top 10 - - return { - "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_tags": top_tags, - } - except Exception as e: raise HTTPException( status_code=500, - detail=f"Failed to fetch user agent summary: {str(e)}", + detail=f"Failed to fetch tag summary analytics: {str(e)}", ) @@ -474,13 +597,13 @@ async def get_user_agent_summary( dependencies=[Depends(user_api_key_auth)], ) async def get_per_user_analytics( - start_date: Optional[str] = Query( + tag_filter: Optional[str] = Query( default=None, - description="Start date in YYYY-MM-DD format", + description="Filter by specific tag (optional)", ), - end_date: Optional[str] = Query( + tag_filters: Optional[List[str]] = Query( default=None, - description="End date in YYYY-MM-DD format", + description="Filter by multiple specific tags (optional, takes precedence over tag_filter)", ), page: int = Query(default=1, description="Page number for pagination", ge=1), page_size: int = Query( @@ -492,16 +615,16 @@ 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 - tag activity during the specified time period. + tag activity during the last 30 days ending on UTC today + 1 day. Args: - start_date: Start date for the analytics period (YYYY-MM-DD) - end_date: End date for the analytics period (YYYY-MM-DD) + tag_filter: Optional filter to specific tag (legacy) + tag_filters: Optional filter to multiple specific tags (takes precedence over tag_filter) page: Page number for pagination page_size: Number of items per page Returns: - PerUserAnalyticsResponse: Analytics data broken down by individual users + PerUserAnalyticsResponse: Analytics data broken down by individual users for the last 30 days """ from litellm.proxy.proxy_server import prisma_client @@ -511,18 +634,30 @@ async def get_per_user_analytics( 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 tag records in the date range + # Calculate end_date as UTC today + 1 day + from datetime import timezone + end_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) + end_date = end_dt.strftime("%Y-%m-%d") + + # Calculate date range (last 30 days) + start_dt = end_dt - timedelta(days=30) + start_date = start_dt.strftime("%Y-%m-%d") + + # Build where clause with date range + where_clause: Dict[str, Any] = { + "date": {"gte": start_date, "lte": end_date} + } + + # Add tag filtering if provided + if tag_filters and len(tag_filters) > 0: + where_clause["tag"] = {"in": tag_filters} + elif tag_filter: + where_clause["tag"] = {"contains": tag_filter} + + # Get all tag records in the date range with optional tag filtering tag_records = await prisma_client.db.litellm_dailytagspend.find_many( - where={ - "date": {"gte": start_date, "lte": end_date} - } + where=where_clause ) # Get unique api_keys diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 537d7d15c7e..c8bc3d2eccb 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -6498,18 +6498,239 @@ export const userAgentAnalyticsCall = async ( } }; +// New endpoint functions for DAU, WAU, MAU +export const tagDauCall = async ( + accessToken: string, + endDate: Date, + tagFilter?: string, + tagFilters?: string[] +) => { + /** + * Get Daily Active Users (DAU) for last 7 days ending on endDate + */ + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/tag/dau` + : `/tag/dau`; + + const queryParams = new URLSearchParams(); + + // Format date 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("end_date", formatDate(endDate)); + + // Handle multiple tag filters (takes precedence over single tag filter) + if (tagFilters && tagFilters.length > 0) { + tagFilters.forEach(tag => { + queryParams.append("tag_filters", tag); + }); + } else if (tagFilter) { + queryParams.append("tag_filter", tagFilter); + } + + 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 DAU:", error); + throw error; + } +}; + +export const tagWauCall = async ( + accessToken: string, + endDate: Date, + tagFilter?: string, + tagFilters?: string[] +) => { + /** + * Get Weekly Active Users (WAU) for last 7 weeks ending on endDate + */ + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/tag/wau` + : `/tag/wau`; + + const queryParams = new URLSearchParams(); + + // Format date 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("end_date", formatDate(endDate)); + + // Handle multiple tag filters (takes precedence over single tag filter) + if (tagFilters && tagFilters.length > 0) { + tagFilters.forEach(tag => { + queryParams.append("tag_filters", tag); + }); + } else if (tagFilter) { + queryParams.append("tag_filter", tagFilter); + } + + 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 WAU:", error); + throw error; + } +}; + +export const tagMauCall = async ( + accessToken: string, + endDate: Date, + tagFilter?: string, + tagFilters?: string[] +) => { + /** + * Get Monthly Active Users (MAU) for last 7 months ending on endDate + */ + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/tag/mau` + : `/tag/mau`; + + const queryParams = new URLSearchParams(); + + // Format date 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("end_date", formatDate(endDate)); + + // Handle multiple tag filters (takes precedence over single tag filter) + if (tagFilters && tagFilters.length > 0) { + tagFilters.forEach(tag => { + queryParams.append("tag_filters", tag); + }); + } else if (tagFilter) { + queryParams.append("tag_filter", tagFilter); + } + + 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 MAU:", error); + throw error; + } +}; + +export const tagDistinctCall = async ( + accessToken: string +) => { + /** + * Get all distinct user agent tags (up to 250) + */ + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/tag/distinct` + : `/tag/distinct`; + + 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 distinct tags:", error); + throw error; + } +}; + export const userAgentSummaryCall = async ( accessToken: string, startTime: Date, - endTime: Date + endTime: Date, + tagFilters?: string[] ) => { /** * Get user agent summary statistics */ try { let url = proxyBaseUrl - ? `${proxyBaseUrl}/tag/user-agent/summary` - : `/tag/user-agent/summary`; + ? `${proxyBaseUrl}/tag/summary` + : `/tag/summary`; const queryParams = new URLSearchParams(); @@ -6524,6 +6745,13 @@ export const userAgentSummaryCall = async ( queryParams.append("start_date", formatDate(startTime)); queryParams.append("end_date", formatDate(endTime)); + // Handle multiple tag filters + if (tagFilters && tagFilters.length > 0) { + tagFilters.forEach(tag => { + queryParams.append("tag_filters", tag); + }); + } + const queryString = queryParams.toString(); if (queryString) { url += `?${queryString}`; @@ -6553,13 +6781,12 @@ export const userAgentSummaryCall = async ( export const perUserAnalyticsCall = async ( accessToken: string, - startTime: Date, - endTime: Date, page: number = 1, - pageSize: number = 50 + pageSize: number = 50, + tagFilters?: string[] ) => { /** - * Get per-user analytics data + * Get per-user analytics data for the last 30 days */ try { let url = proxyBaseUrl @@ -6568,19 +6795,16 @@ export const perUserAnalyticsCall = async ( 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()); + // Handle multiple tag filters + if (tagFilters && tagFilters.length > 0) { + tagFilters.forEach(tag => { + queryParams.append("tag_filters", tag); + }); + } + const queryString = queryParams.toString(); if (queryString) { url += `?${queryString}`; diff --git a/ui/litellm-dashboard/src/components/per_user_usage.tsx b/ui/litellm-dashboard/src/components/per_user_usage.tsx index 1d019e2f59d..1d074b8a1d0 100644 --- a/ui/litellm-dashboard/src/components/per_user_usage.tsx +++ b/ui/litellm-dashboard/src/components/per_user_usage.tsx @@ -42,13 +42,13 @@ interface PerUserAnalyticsResponse { interface PerUserUsageProps { accessToken: string | null; - dateValue: DateRangePickerValue; + selectedTags: string[]; formatAbbreviatedNumber: (value: number, decimalPlaces?: number) => string; } const PerUserUsage: React.FC = ({ accessToken, - dateValue, + selectedTags, formatAbbreviatedNumber, }) => { const [perUserData, setPerUserData] = useState({ @@ -63,16 +63,15 @@ const PerUserUsage: React.FC = ({ const [currentPage, setCurrentPage] = useState(1); const fetchPerUserData = async () => { - if (!accessToken || !dateValue.from || !dateValue.to) return; + if (!accessToken) return; setLoading(true); try { const response = await perUserAnalyticsCall( accessToken, - dateValue.from, - dateValue.to, currentPage, - 50 + 50, + selectedTags.length > 0 ? selectedTags : undefined ); setPerUserData(response); } catch (error) { @@ -84,7 +83,7 @@ const PerUserUsage: React.FC = ({ useEffect(() => { fetchPerUserData(); - }, [accessToken, dateValue, currentPage]); + }, [accessToken, selectedTags, currentPage]); const handleNextPage = () => { if (currentPage < perUserData.total_pages) { diff --git a/ui/litellm-dashboard/src/components/user_agent_activity.tsx b/ui/litellm-dashboard/src/components/user_agent_activity.tsx index 0d0eda36159..ebc069783ee 100644 --- a/ui/litellm-dashboard/src/components/user_agent_activity.tsx +++ b/ui/litellm-dashboard/src/components/user_agent_activity.tsx @@ -15,8 +15,6 @@ import { DonutChart, Metric, Subtitle, - Select, - SelectItem, Button, Tab, TabGroup, @@ -24,55 +22,47 @@ import { TabPanel, TabPanels, } from "@tremor/react"; +import { Select } from "antd"; import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { userAgentAnalyticsCall, userAgentSummaryCall } from "./networking"; +import { userAgentSummaryCall, tagDauCall, tagWauCall, tagMauCall, tagDistinctCall } from "./networking"; 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; - wau: number; - mau: number; +// New interfaces for the updated API response +interface TagActiveUsersResponse { + tag: string; + active_users: number; + date: string; + period_start?: string; + period_end?: string; +} + +interface ActiveUsersAnalyticsResponse { + results: TagActiveUsersResponse[]; +} + +interface TagSummaryMetrics { + tag: string; + unique_users: number; + total_requests: number; successful_requests: number; failed_requests: number; - total_requests: number; - completed_tokens: number; - total_tokens: number; - spend: number; -} - -interface UserAgentActivityData { - date: string; - tag: string; - user_agent: string; - metrics: UserAgentMetrics; -} - -interface UserAgentAnalyticsResponse { - results: UserAgentActivityData[]; - total_count: number; - page: number; - page_size: number; - total_pages: number; -} - -interface UserAgentSummaryData { - total_tags: number; - total_requests: number; - total_successful_requests: number; - total_failed_requests: number; total_tokens: number; total_spend: number; - top_tags: Array<{ - tag: string; - requests: number; - successful_requests: number; - failed_requests: number; - tokens: number; - spend: number; - }>; +} + +interface TagSummaryResponse { + results: TagSummaryMetrics[]; +} + +interface DistinctTagResponse { + tag: string; +} + +interface DistinctTagsResponse { + results: DistinctTagResponse[]; } interface UserAgentActivityProps { @@ -84,23 +74,11 @@ const UserAgentActivity: React.FC = ({ accessToken, userRole, }) => { - const [analyticsData, setAnalyticsData] = useState({ - results: [], - total_count: 0, - page: 1, - page_size: 50, - total_pages: 0, - }); - - const [summaryData, setSummaryData] = useState({ - total_tags: 0, - total_requests: 0, - total_successful_requests: 0, - total_failed_requests: 0, - total_tokens: 0, - total_spend: 0, - top_tags: [], - }); + // Separate state for each endpoint + const [dauData, setDauData] = useState({ results: [] }); + const [wauData, setWauData] = useState({ results: [] }); + const [mauData, setMauData] = useState({ results: [] }); + const [summaryData, setSummaryData] = useState({ results: [] }); const [dateValue, setDateValue] = useState({ from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), @@ -108,31 +86,91 @@ const UserAgentActivity: React.FC = ({ }); const [userAgentFilter, setUserAgentFilter] = useState(""); - const [analyticsLoading, setAnalyticsLoading] = useState(false); + + // Tag filtering state + const [availableTags, setAvailableTags] = useState([]); + const [selectedTags, setSelectedTags] = useState([]); + const [tagsLoading, setTagsLoading] = useState(false); + + // Separate loading states for each endpoint + const [dauLoading, setDauLoading] = useState(false); + const [wauLoading, setWauLoading] = useState(false); + const [mauLoading, setMauLoading] = useState(false); const [summaryLoading, setSummaryLoading] = useState(false); + const [isDateChanging, setIsDateChanging] = useState(false); - const [currentPage, setCurrentPage] = useState(1); - const fetchAnalyticsData = async () => { - if (!accessToken || !dateValue.from || !dateValue.to) return; + // Use today's date as the end date for all API calls + const today = new Date(); - setAnalyticsLoading(true); + const fetchAvailableTags = async () => { + if (!accessToken) return; + + setTagsLoading(true); try { - const analytics = await userAgentAnalyticsCall( - accessToken, - dateValue.from, - dateValue.to, - currentPage, - 50, - userAgentFilter || undefined - ); - - setAnalyticsData(analytics); + const data = await tagDistinctCall(accessToken); + setAvailableTags(data.results.map((item: DistinctTagResponse) => item.tag)); } catch (error) { - console.error("Failed to fetch user agent analytics data:", error); + console.error("Failed to fetch available tags:", error); } finally { - setAnalyticsLoading(false); - setIsDateChanging(false); + setTagsLoading(false); + } + }; + + const fetchDauData = async () => { + if (!accessToken) return; + + setDauLoading(true); + try { + const data = await tagDauCall( + accessToken, + today, + userAgentFilter || undefined, + selectedTags.length > 0 ? selectedTags : undefined + ); + setDauData(data); + } catch (error) { + console.error("Failed to fetch DAU data:", error); + } finally { + setDauLoading(false); + } + }; + + const fetchWauData = async () => { + if (!accessToken) return; + + setWauLoading(true); + try { + const data = await tagWauCall( + accessToken, + today, + userAgentFilter || undefined, + selectedTags.length > 0 ? selectedTags : undefined + ); + setWauData(data); + } catch (error) { + console.error("Failed to fetch WAU data:", error); + } finally { + setWauLoading(false); + } + }; + + const fetchMauData = async () => { + if (!accessToken) return; + + setMauLoading(true); + try { + const data = await tagMauCall( + accessToken, + today, + userAgentFilter || undefined, + selectedTags.length > 0 ? selectedTags : undefined + ); + setMauData(data); + } catch (error) { + console.error("Failed to fetch MAU data:", error); + } finally { + setMauLoading(false); } }; @@ -141,7 +179,12 @@ const UserAgentActivity: React.FC = ({ setSummaryLoading(true); try { - const summary = await userAgentSummaryCall(accessToken, dateValue.from, dateValue.to); + const summary = await userAgentSummaryCall( + accessToken, + dateValue.from, + dateValue.to, + selectedTags.length > 0 ? selectedTags : undefined + ); setSummaryData(summary); } catch (error) { console.error("Failed to fetch user agent summary data:", error); @@ -155,69 +198,40 @@ const UserAgentActivity: React.FC = ({ 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 + // Effect to fetch available tags on mount useEffect(() => { - if (!dateValue.from || !dateValue.to) return; + fetchAvailableTags(); + }, [accessToken]); + + // Effect for DAU/WAU/MAU data (independent of date picker) + useEffect(() => { + if (!accessToken) return; const timeoutId = setTimeout(() => { - // Call both fetch functions independently - fetchAnalyticsData(); - fetchSummaryData(); - }, 50); // Very short debounce - - 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(); + fetchDauData(); + fetchWauData(); + fetchMauData(); }, 50); return () => clearTimeout(timeoutId); - }, [currentPage]); + }, [accessToken, userAgentFilter, selectedTags]); - // Aggregate data by user agent for charts - const aggregatedByUserAgent = analyticsData.results.reduce((acc, item) => { - const ua = item.user_agent || "Unknown"; - if (!acc[ua]) { - acc[ua] = { - user_agent: ua, - total_requests: 0, - successful_requests: 0, - failed_requests: 0, - total_tokens: 0, - spend: 0, - dau: 0, - wau: 0, - mau: 0, - }; - } - acc[ua].total_requests += item.metrics.total_requests; - acc[ua].successful_requests += item.metrics.successful_requests; - acc[ua].failed_requests += item.metrics.failed_requests; - acc[ua].total_tokens += item.metrics.total_tokens; - acc[ua].spend += item.metrics.spend; - // For user counts, take the maximum to avoid double counting - acc[ua].dau = Math.max(acc[ua].dau, item.metrics.dau); - acc[ua].wau = Math.max(acc[ua].wau, item.metrics.wau); - acc[ua].mau = Math.max(acc[ua].mau, item.metrics.mau); - return acc; - }, {} as Record); + // Effect for summary data (depends on date picker) + useEffect(() => { + if (!dateValue.from || !dateValue.to) return; - const chartData = Object.values(aggregatedByUserAgent).sort( - (a: any, b: any) => b.total_requests - a.total_requests - ); + const timeoutId = setTimeout(() => { + fetchSummaryData(); + }, 50); + + return () => clearTimeout(timeoutId); + }, [accessToken, dateValue, selectedTags]); // Helper function to extract user agent from tag const extractUserAgent = (tag: string): string => { @@ -235,62 +249,133 @@ const UserAgentActivity: React.FC = ({ 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 - 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 { - const newDateEntry: any = { - date: item.date, - [item.user_agent || "Unknown"]: item.metrics.dau - }; - acc.push(newDateEntry); + // Get all user agents for each chart type based on their specific data + const getAllTagsForData = (data: TagActiveUsersResponse[]) => { + // Aggregate total active users per tag + const tagTotals = data.reduce((acc, item) => { + acc[item.tag] = (acc[item.tag] || 0) + item.active_users; + return acc; + }, {} as Record); + + // Sort by total active users and return all tags + return Object.entries(tagTotals) + .sort(([, a], [, b]) => b - a) + .map(([tag]) => tag); + }; + + const allDauTags = getAllTagsForData(dauData.results); + const allWauTags = getAllTagsForData(wauData.results); + const allMauTags = getAllTagsForData(mauData.results); + + // Prepare daily chart data (DAU) - always show last 7 days + const generateDailyChartData = () => { + const chartData: any[] = []; + const endDate = new Date(); + + // Generate all 7 days + for (let i = 6; i >= 0; i--) { + const date = new Date(endDate); + date.setDate(date.getDate() - i); + const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD format + + const dayEntry: any = { date: dateStr }; + + // Initialize all user agents to 0 + allDauTags.forEach(tag => { + const userAgent = extractUserAgent(tag); + dayEntry[userAgent] = 0; + }); + + chartData.push(dayEntry); } - return acc; - }, [] as any[]); + + // Fill in actual data + dauData.results.forEach(item => { + const userAgent = extractUserAgent(item.tag); + const dayEntry = chartData.find(d => d.date === item.date); + if (dayEntry) { + dayEntry[userAgent] = item.active_users; + } + }); + + return chartData; + }; + + const dailyChartData = generateDailyChartData(); - // 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); + // Prepare weekly chart data (WAU) - always show all 7 weeks + const generateWeeklyChartData = () => { + const chartData: any[] = []; + + // Generate all 7 weeks (Week 1 through Week 7) + for (let weekNum = 1; weekNum <= 7; weekNum++) { + const weekEntry: any = { week: `Week ${weekNum}` }; + + // Initialize all user agents to 0 + allWauTags.forEach(tag => { + const userAgent = extractUserAgent(tag); + weekEntry[userAgent] = 0; + }); + + chartData.push(weekEntry); } - return acc; - }, [] as any[]); + + // Fill in actual data + wauData.results.forEach(item => { + const userAgent = extractUserAgent(item.tag); + // Extract week number from the date field (e.g., "Week 1 (Jul 27)" -> "Week 1") + const weekMatch = item.date.match(/Week (\d+)/); + if (weekMatch) { + const weekLabel = `Week ${weekMatch[1]}`; + const weekEntry = chartData.find(d => d.week === weekLabel); + if (weekEntry) { + weekEntry[userAgent] = item.active_users; + } + } + }); + + return chartData; + }; + + const weeklyChartData = generateWeeklyChartData(); - // 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); + // Prepare monthly chart data (MAU) - always show all 7 months + const generateMonthlyChartData = () => { + const chartData: any[] = []; + + // Generate all 7 months (Month 1 through Month 7) + for (let monthNum = 1; monthNum <= 7; monthNum++) { + const monthEntry: any = { month: `Month ${monthNum}` }; + + // Initialize all user agents to 0 + allMauTags.forEach(tag => { + const userAgent = extractUserAgent(tag); + monthEntry[userAgent] = 0; + }); + + chartData.push(monthEntry); } - return acc; - }, [] as any[]); + + // Fill in actual data + mauData.results.forEach(item => { + const userAgent = extractUserAgent(item.tag); + // Extract month number from the date field (e.g., "Month 1 (Jul)" -> "Month 1") + const monthMatch = item.date.match(/Month (\d+)/); + if (monthMatch) { + const monthLabel = `Month ${monthMatch[1]}`; + const monthEntry = chartData.find(d => d.month === monthLabel); + if (monthEntry) { + monthEntry[userAgent] = item.active_users; + } + } + }); + + return chartData; + }; + + const monthlyChartData = generateMonthlyChartData(); // Format numbers with K, M abbreviations const formatAbbreviatedNumber = (value: number, decimalPlaces: number = 0): string => { @@ -310,93 +395,112 @@ const UserAgentActivity: React.FC = ({ }; return ( -
- {/* Date Range Picker */} - - +
+ {/* Summary Section Card */} + +
+
+
+ Summary by User Agent + Performance metrics for different user agents +
+ + {/* User Agent Filter */} +
+ Filter by User Agents + +
+
+ + {/* Date Range Picker within Summary */} - - - - - - {/* Top 4 User Agents Cards */} - {summaryLoading ? ( - - - - ) : ( - - {(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)} + {/* Top 4 User Agents Cards */} + {summaryLoading ? ( + + ) : ( + + {(summaryData.results || []).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.total_tokens)} +
+
+ Total Cost + ${formatAbbreviatedNumber(tag.total_spend, 4)} +
+
+
+ ); + })} + {/* Fill remaining slots if less than 4 agents */} + {Array.from({ length: Math.max(0, 4 - (summaryData.results || []).length) }).map((_, index) => ( + + No Data +
+
+ Success Requests + - +
+
+ Total Tokens + - +
+
+ Total Cost + - +
-
- 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 - - -
-
-
- ))} - - )} + + ))} + + )} +
+
{/* Main TabGroup for DAU/WAU/MAU vs Per User Usage */} DAU/WAU/MAU - Per User Usage + Per User Usage (Last 30 Days) @@ -419,36 +523,36 @@ const UserAgentActivity: React.FC = ({
Daily Active Users - Last 7 Days
- {analyticsLoading ? ( - + {dauLoading ? ( + ) : ( formatAbbreviatedNumber(value)} yAxisWidth={60} showLegend={true} + stack={true} /> )}
- Weekly Active Users - Last 4 Weeks + Weekly Active Users - Last 7 Weeks
- {analyticsLoading ? ( - + {wauLoading ? ( + ) : ( formatAbbreviatedNumber(value)} yAxisWidth={60} showLegend={true} + stack={true} /> )}
@@ -457,17 +561,17 @@ const UserAgentActivity: React.FC = ({
Monthly Active Users - Last 7 Months
- {analyticsLoading ? ( - + {mauLoading ? ( + ) : ( formatAbbreviatedNumber(value)} yAxisWidth={60} showLegend={true} + stack={true} /> )} @@ -479,7 +583,7 @@ const UserAgentActivity: React.FC = ({