mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
[UI QA] QA - Agent Activity Tab (#13203)
* backend fixes * fixes for User-Agent ui * UI fixes chart loader * fixes chart loader * fixes ChartLoader * fix ChartLoader * fixes for analytics
This commit is contained in:
parent
d887a3d102
commit
74e29ae153
4 changed files with 273 additions and 247 deletions
|
|
@ -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: <agent_name>" or "User-Agent: <agent_name>/<version>"
|
||||
"""
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<NewUsagePageProps> = ({ accessToken, userRole, user
|
|||
return () => clearTimeout(timeoutId)
|
||||
}, [fetchUserSpendData])
|
||||
|
||||
// Enhanced loading component with better visual feedback
|
||||
const ChartLoader = () => (
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<UiLoadingSpinner className="size-5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-600 text-sm font-medium">
|
||||
{isDateChanging ? "Processing date selection..." : "Loading chart data..."}
|
||||
</span>
|
||||
<span className="text-gray-400 text-xs mt-1">
|
||||
{isDateChanging ? "This will only take a moment" : "Fetching your data"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
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<NewUsagePageProps> = ({ accessToken, userRole, user
|
|||
<Card>
|
||||
<Title>Daily Spend</Title>
|
||||
{loading ? (
|
||||
<ChartLoader />
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={[...userSpendData.results].sort(
|
||||
|
|
@ -606,7 +589,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({ accessToken, userRole, user
|
|||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<ChartLoader />
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<BarChart
|
||||
className="mt-4 h-40"
|
||||
|
|
@ -646,7 +629,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({ accessToken, userRole, user
|
|||
<Title>Spend by Provider</Title>
|
||||
</div>
|
||||
{loading ? (
|
||||
<ChartLoader />
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<Grid numItems={2}>
|
||||
<Col numColSpan={1}>
|
||||
|
|
|
|||
24
ui/litellm-dashboard/src/components/shared/chart_loader.tsx
Normal file
24
ui/litellm-dashboard/src/components/shared/chart_loader.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from "react";
|
||||
import { UiLoadingSpinner } from "../ui/ui-loading-spinner";
|
||||
|
||||
interface ChartLoaderProps {
|
||||
isDateChanging?: boolean;
|
||||
}
|
||||
|
||||
export const ChartLoader: React.FC<ChartLoaderProps> = ({ isDateChanging = false }) => (
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<UiLoadingSpinner className="size-5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-600 text-sm font-medium">
|
||||
{isDateChanging ? "Processing date selection..." : "Loading chart data..."}
|
||||
</span>
|
||||
<span className="text-gray-400 text-xs mt-1">
|
||||
{isDateChanging ? "This will only take a moment" : "Fetching your data"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ChartLoader;
|
||||
|
|
@ -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<UserAgentActivityProps> = ({
|
|||
});
|
||||
|
||||
const [summaryData, setSummaryData] = useState<UserAgentSummaryData>({
|
||||
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<DateRangePickerValue>({
|
||||
|
|
@ -107,50 +108,84 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
|
|||
});
|
||||
|
||||
const [userAgentFilter, setUserAgentFilter] = useState<string>("");
|
||||
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<UserAgentActivityProps> = ({
|
|||
(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<UserAgentActivityProps> = ({
|
|||
{/* Date Range Picker */}
|
||||
<Grid numItems={2} className="gap-2 w-full">
|
||||
<Col>
|
||||
<UsageDatePicker
|
||||
<AdvancedDatePicker
|
||||
value={dateValue}
|
||||
onValueChange={(value) => {
|
||||
setDateValue(value);
|
||||
setCurrentPage(1); // Reset to first page when date changes
|
||||
}}
|
||||
onValueChange={handleDateChange}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -289,49 +337,59 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
|
|||
</Grid>
|
||||
|
||||
{/* Top 4 User Agents Cards */}
|
||||
<Grid numItems={4} className="gap-4">
|
||||
{summaryData.top_user_agents.slice(0, 4).map((ua, index) => (
|
||||
<Card key={index}>
|
||||
<Title className="truncate" title={ua.user_agent}>
|
||||
{ua.user_agent}
|
||||
</Title>
|
||||
<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>
|
||||
{summaryLoading ? (
|
||||
<Card>
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
</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>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Success Requests</Text>
|
||||
<Metric className="text-lg">-</Metric>
|
||||
) : (
|
||||
<Grid numItems={4} className="gap-4">
|
||||
{(summaryData.top_tags || []).slice(0, 4).map((tag, index) => {
|
||||
const userAgent = extractUserAgent(tag.tag);
|
||||
const displayName = truncateUserAgent(userAgent);
|
||||
return (
|
||||
<Card key={index}>
|
||||
<Title className="truncate" title={userAgent}>
|
||||
{displayName}
|
||||
</Title>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Success Requests</Text>
|
||||
<Metric className="text-lg">{formatAbbreviatedNumber(tag.successful_requests)}</Metric>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Tokens</Text>
|
||||
<Metric className="text-lg">{formatAbbreviatedNumber(tag.tokens)}</Metric>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Cost</Text>
|
||||
<Metric className="text-lg">${formatAbbreviatedNumber(tag.spend, 4)}</Metric>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{/* Fill remaining slots if less than 4 agents */}
|
||||
{Array.from({ length: Math.max(0, 4 - (summaryData.top_tags || []).length) }).map((_, index) => (
|
||||
<Card key={`empty-${index}`}>
|
||||
<Title>No Data</Title>
|
||||
<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>
|
||||
<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>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Main TabGroup for DAU/WAU/MAU vs Per User Usage */}
|
||||
<Card>
|
||||
|
|
@ -361,45 +419,57 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
|
|||
<div className="mb-4">
|
||||
<Title className="text-lg">Daily Active Users - Last 7 Days</Title>
|
||||
</div>
|
||||
<BarChart
|
||||
data={dailyChartData}
|
||||
index="date"
|
||||
categories={uniqueUserAgents.slice(0, 3)}
|
||||
colors={["blue", "green", "orange"]}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
/>
|
||||
{analyticsLoading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<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>
|
||||
<BarChart
|
||||
data={weeklyChartData}
|
||||
index="week"
|
||||
categories={uniqueUserAgents.slice(0, 3)}
|
||||
colors={["blue", "green", "orange"]}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
/>
|
||||
{analyticsLoading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<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}
|
||||
/>
|
||||
{analyticsLoading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue