mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
[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
This commit is contained in:
parent
3d02d3602e
commit
4a85a91f20
4 changed files with 1152 additions and 690 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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<PerUserUsageProps> = ({
|
||||
accessToken,
|
||||
dateValue,
|
||||
selectedTags,
|
||||
formatAbbreviatedNumber,
|
||||
}) => {
|
||||
const [perUserData, setPerUserData] = useState<PerUserAnalyticsResponse>({
|
||||
|
|
@ -63,16 +63,15 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({
|
|||
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<PerUserUsageProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
fetchPerUserData();
|
||||
}, [accessToken, dateValue, currentPage]);
|
||||
}, [accessToken, selectedTags, currentPage]);
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (currentPage < perUserData.total_pages) {
|
||||
|
|
|
|||
|
|
@ -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<UserAgentActivityProps> = ({
|
|||
accessToken,
|
||||
userRole,
|
||||
}) => {
|
||||
const [analyticsData, setAnalyticsData] = useState<UserAgentAnalyticsResponse>({
|
||||
results: [],
|
||||
total_count: 0,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
total_pages: 0,
|
||||
});
|
||||
|
||||
const [summaryData, setSummaryData] = useState<UserAgentSummaryData>({
|
||||
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<ActiveUsersAnalyticsResponse>({ results: [] });
|
||||
const [wauData, setWauData] = useState<ActiveUsersAnalyticsResponse>({ results: [] });
|
||||
const [mauData, setMauData] = useState<ActiveUsersAnalyticsResponse>({ results: [] });
|
||||
const [summaryData, setSummaryData] = useState<TagSummaryResponse>({ results: [] });
|
||||
|
||||
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
|
||||
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
|
|
@ -108,31 +86,91 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
|
|||
});
|
||||
|
||||
const [userAgentFilter, setUserAgentFilter] = useState<string>("");
|
||||
const [analyticsLoading, setAnalyticsLoading] = useState(false);
|
||||
|
||||
// Tag filtering state
|
||||
const [availableTags, setAvailableTags] = useState<string[]>([]);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
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<UserAgentActivityProps> = ({
|
|||
|
||||
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<UserAgentActivityProps> = ({
|
|||
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<string, any>);
|
||||
// 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<UserAgentActivityProps> = ({
|
|||
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<string, number>);
|
||||
|
||||
// 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<UserAgentActivityProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Date Range Picker */}
|
||||
<Grid numItems={2} className="gap-2 w-full">
|
||||
<Col>
|
||||
<div className="space-y-6 mt-6">
|
||||
{/* Summary Section Card */}
|
||||
<Card>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<Title>Summary by User Agent</Title>
|
||||
<Subtitle>Performance metrics for different user agents</Subtitle>
|
||||
</div>
|
||||
|
||||
{/* User Agent Filter */}
|
||||
<div className="w-96">
|
||||
<Text className="text-sm font-medium block mb-2">Filter by User Agents</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="All User Agents"
|
||||
value={selectedTags}
|
||||
onChange={setSelectedTags}
|
||||
style={{ width: "100%" }}
|
||||
showSearch={true}
|
||||
allowClear={true}
|
||||
loading={tagsLoading}
|
||||
optionFilterProp="label"
|
||||
className="rounded-md"
|
||||
maxTagCount="responsive"
|
||||
>
|
||||
{availableTags.map((tag) => {
|
||||
const userAgent = extractUserAgent(tag);
|
||||
const displayName = userAgent.length > 50 ? `${userAgent.substring(0, 50)}...` : userAgent;
|
||||
return (
|
||||
<Select.Option key={tag} value={tag} label={displayName} title={userAgent}>
|
||||
{displayName}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Range Picker within Summary */}
|
||||
<AdvancedDatePicker
|
||||
value={dateValue}
|
||||
onValueChange={handleDateChange}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Select
|
||||
value={userAgentFilter}
|
||||
onValueChange={setUserAgentFilter}
|
||||
placeholder="Filter by User Agent"
|
||||
>
|
||||
<SelectItem value="">All User Agents</SelectItem>
|
||||
<SelectItem value="curl">curl</SelectItem>
|
||||
<SelectItem value="litellm">litellm</SelectItem>
|
||||
<SelectItem value="python">python</SelectItem>
|
||||
<SelectItem value="postman">postman</SelectItem>
|
||||
<SelectItem value="axios">axios</SelectItem>
|
||||
<SelectItem value="fetch">fetch</SelectItem>
|
||||
</Select>
|
||||
</Col>
|
||||
</Grid>
|
||||
|
||||
{/* Top 4 User Agents Cards */}
|
||||
{summaryLoading ? (
|
||||
<Card>
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
</Card>
|
||||
) : (
|
||||
<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>
|
||||
{/* Top 4 User Agents Cards */}
|
||||
{summaryLoading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<Grid numItems={4} className="gap-4">
|
||||
{(summaryData.results || []).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.total_tokens)}</Metric>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm text-gray-600">Total Cost</Text>
|
||||
<Metric className="text-lg">${formatAbbreviatedNumber(tag.total_spend, 4)}</Metric>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{/* Fill remaining slots if less than 4 agents */}
|
||||
{Array.from({ length: Math.max(0, 4 - (summaryData.results || []).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">{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>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Main TabGroup for DAU/WAU/MAU vs Per User Usage */}
|
||||
<Card>
|
||||
<TabGroup>
|
||||
<TabList className="mb-6">
|
||||
<Tab>DAU/WAU/MAU</Tab>
|
||||
<Tab>Per User Usage</Tab>
|
||||
<Tab>Per User Usage (Last 30 Days)</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
|
|
@ -419,36 +523,36 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
|
|||
<div className="mb-4">
|
||||
<Title className="text-lg">Daily Active Users - Last 7 Days</Title>
|
||||
</div>
|
||||
{analyticsLoading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
{dauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={dailyChartData}
|
||||
index="date"
|
||||
categories={uniqueUserAgents.slice(0, 3)}
|
||||
colors={["blue", "green", "orange"]}
|
||||
categories={allDauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<div className="mb-4">
|
||||
<Title className="text-lg">Weekly Active Users - Last 4 Weeks</Title>
|
||||
<Title className="text-lg">Weekly Active Users - Last 7 Weeks</Title>
|
||||
</div>
|
||||
{analyticsLoading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
{wauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={weeklyChartData}
|
||||
index="week"
|
||||
categories={uniqueUserAgents.slice(0, 3)}
|
||||
colors={["blue", "green", "orange"]}
|
||||
categories={allWauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
|
@ -457,17 +561,17 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
|
|||
<div className="mb-4">
|
||||
<Title className="text-lg">Monthly Active Users - Last 7 Months</Title>
|
||||
</div>
|
||||
{analyticsLoading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
{mauLoading ? (
|
||||
<ChartLoader isDateChanging={false} />
|
||||
) : (
|
||||
<BarChart
|
||||
data={monthlyChartData}
|
||||
index="month"
|
||||
categories={uniqueUserAgents.slice(0, 3)}
|
||||
colors={["blue", "green", "orange"]}
|
||||
categories={allMauTags.map(extractUserAgent)}
|
||||
valueFormatter={(value: number) => formatAbbreviatedNumber(value)}
|
||||
yAxisWidth={60}
|
||||
showLegend={true}
|
||||
stack={true}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
|
@ -479,7 +583,7 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({
|
|||
<TabPanel>
|
||||
<PerUserUsage
|
||||
accessToken={accessToken}
|
||||
dateValue={dateValue}
|
||||
selectedTags={selectedTags}
|
||||
formatAbbreviatedNumber={formatAbbreviatedNumber}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue