diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py index 5074b3042..c00c6f750 100644 --- a/backend/open_webui/models/chat_messages.py +++ b/backend/open_webui/models/chat_messages.py @@ -312,10 +312,12 @@ class ChatMessageTable: self, start_date: Optional[int] = None, end_date: Optional[int] = None, + group_id: Optional[str] = None, db: Optional[Session] = None, ) -> dict[str, int]: with get_db_context(db) as db: from sqlalchemy import func + from open_webui.models.groups import GroupMember query = db.query( ChatMessage.model_id, func.count(ChatMessage.id).label("count") @@ -329,6 +331,9 @@ class ChatMessageTable: query = query.filter(ChatMessage.created_at >= start_date) if end_date: query = query.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() + query = query.filter(ChatMessage.user_id.in_(group_users)) results = query.group_by(ChatMessage.model_id).all() return {row.model_id: row.count for row in results} @@ -337,11 +342,13 @@ class ChatMessageTable: self, start_date: Optional[int] = None, end_date: Optional[int] = None, + group_id: Optional[str] = None, db: Optional[Session] = None, ) -> dict[str, dict]: """Aggregate token usage by model using database-level aggregation.""" with get_db_context(db) as db: from sqlalchemy import func, cast, Integer + from open_webui.models.groups import GroupMember dialect = db.bind.dialect.name @@ -379,6 +386,9 @@ class ChatMessageTable: query = query.filter(ChatMessage.created_at >= start_date) if end_date: query = query.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() + query = query.filter(ChatMessage.user_id.in_(group_users)) results = query.group_by(ChatMessage.model_id).all() @@ -455,10 +465,12 @@ class ChatMessageTable: self, start_date: Optional[int] = None, end_date: Optional[int] = None, + group_id: Optional[str] = None, db: Optional[Session] = None, ) -> dict[str, int]: with get_db_context(db) as db: from sqlalchemy import func + from open_webui.models.groups import GroupMember query = db.query( ChatMessage.user_id, func.count(ChatMessage.id).label("count") @@ -468,6 +480,9 @@ class ChatMessageTable: query = query.filter(ChatMessage.created_at >= start_date) if end_date: query = query.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() + query = query.filter(ChatMessage.user_id.in_(group_users)) results = query.group_by(ChatMessage.user_id).all() return {row.user_id: row.count for row in results} @@ -476,10 +491,12 @@ class ChatMessageTable: self, start_date: Optional[int] = None, end_date: Optional[int] = None, + group_id: Optional[str] = None, db: Optional[Session] = None, ) -> dict[str, int]: with get_db_context(db) as db: from sqlalchemy import func + from open_webui.models.groups import GroupMember query = db.query( ChatMessage.chat_id, func.count(ChatMessage.id).label("count") @@ -489,6 +506,9 @@ class ChatMessageTable: query = query.filter(ChatMessage.created_at >= start_date) if end_date: query = query.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() + query = query.filter(ChatMessage.user_id.in_(group_users)) results = query.group_by(ChatMessage.chat_id).all() return {row.chat_id: row.count for row in results} @@ -497,11 +517,13 @@ class ChatMessageTable: self, start_date: Optional[int] = None, end_date: Optional[int] = None, + group_id: Optional[str] = None, db: Optional[Session] = None, ) -> dict[str, dict[str, int]]: """Get message counts grouped by day and model.""" with get_db_context(db) as db: from datetime import datetime, timedelta + from open_webui.models.groups import GroupMember query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter( ChatMessage.role == "assistant", @@ -513,6 +535,9 @@ class ChatMessageTable: query = query.filter(ChatMessage.created_at >= start_date) if end_date: query = query.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() + query = query.filter(ChatMessage.user_id.in_(group_users)) results = query.all() diff --git a/backend/open_webui/routers/analytics.py b/backend/open_webui/routers/analytics.py index 35e4aead2..a78431f49 100644 --- a/backend/open_webui/routers/analytics.py +++ b/backend/open_webui/routers/analytics.py @@ -7,6 +7,7 @@ from pydantic import BaseModel from open_webui.models.chat_messages import ChatMessages, ChatMessageModel from open_webui.models.chats import Chats +from open_webui.models.groups import Groups from open_webui.models.users import Users from open_webui.models.feedbacks import Feedbacks from open_webui.utils.auth import get_admin_user @@ -56,12 +57,13 @@ class UserAnalyticsResponse(BaseModel): async def get_model_analytics( start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"), end_date: Optional[int] = Query(None, description="End timestamp (epoch)"), + group_id: Optional[str] = Query(None, description="Filter by user group ID"), user=Depends(get_admin_user), db: Session = Depends(get_session), ): """Get message counts per model.""" counts = ChatMessages.get_message_count_by_model( - start_date=start_date, end_date=end_date, db=db + start_date=start_date, end_date=end_date, group_id=group_id, db=db ) models = [ ModelAnalyticsEntry(model_id=model_id, count=count) @@ -74,15 +76,14 @@ async def get_model_analytics( async def get_user_analytics( start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"), end_date: Optional[int] = Query(None, description="End timestamp (epoch)"), + group_id: Optional[str] = Query(None, description="Filter by user group ID"), limit: int = Query(50, description="Max users to return"), user=Depends(get_admin_user), db: Session = Depends(get_session), ): """Get message counts and token usage per user with user info.""" - from open_webui.models.users import Users - counts = ChatMessages.get_message_count_by_user( - start_date=start_date, end_date=end_date, db=db + start_date=start_date, end_date=end_date, group_id=group_id, db=db ) token_usage = ChatMessages.get_token_usage_by_user( start_date=start_date, end_date=end_date, db=db @@ -153,18 +154,19 @@ class SummaryResponse(BaseModel): async def get_summary( start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"), end_date: Optional[int] = Query(None, description="End timestamp (epoch)"), + group_id: Optional[str] = Query(None, description="Filter by user group ID"), user=Depends(get_admin_user), db: Session = Depends(get_session), ): """Get summary statistics for the dashboard.""" model_counts = ChatMessages.get_message_count_by_model( - start_date=start_date, end_date=end_date, db=db + start_date=start_date, end_date=end_date, group_id=group_id, db=db ) user_counts = ChatMessages.get_message_count_by_user( - start_date=start_date, end_date=end_date, db=db + start_date=start_date, end_date=end_date, group_id=group_id, db=db ) chat_counts = ChatMessages.get_message_count_by_chat( - start_date=start_date, end_date=end_date, db=db + start_date=start_date, end_date=end_date, group_id=group_id, db=db ) return SummaryResponse( @@ -188,6 +190,7 @@ class DailyStatsResponse(BaseModel): async def get_daily_stats( start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"), end_date: Optional[int] = Query(None, description="End timestamp (epoch)"), + group_id: Optional[str] = Query(None, description="Filter by user group ID"), granularity: str = Query("daily", description="Granularity: 'hourly' or 'daily'"), user=Depends(get_admin_user), db: Session = Depends(get_session), @@ -199,7 +202,7 @@ async def get_daily_stats( ) else: counts = ChatMessages.get_daily_message_counts_by_model( - start_date=start_date, end_date=end_date, db=db + start_date=start_date, end_date=end_date, group_id=group_id, db=db ) return DailyStatsResponse( data=[ @@ -228,12 +231,13 @@ class TokenUsageResponse(BaseModel): async def get_token_usage( start_date: Optional[int] = Query(None), end_date: Optional[int] = Query(None), + group_id: Optional[str] = Query(None, description="Filter by user group ID"), user=Depends(get_admin_user), db: Session = Depends(get_session), ): """Get token usage aggregated by model.""" usage = ChatMessages.get_token_usage_by_model( - start_date=start_date, end_date=end_date, db=db + start_date=start_date, end_date=end_date, group_id=group_id, db=db ) models = [ diff --git a/src/lib/apis/analytics/index.ts b/src/lib/apis/analytics/index.ts index c678d54e7..f03729937 100644 --- a/src/lib/apis/analytics/index.ts +++ b/src/lib/apis/analytics/index.ts @@ -3,13 +3,15 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; export const getModelAnalytics = async ( token: string = '', startDate: number | null = null, - endDate: number | null = null + endDate: number | null = null, + groupId: string | null = null ) => { let error = null; const searchParams = new URLSearchParams(); if (startDate) searchParams.append('start_date', startDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString()); + if (groupId) searchParams.append('group_id', groupId); const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models?${searchParams.toString()}`, { method: 'GET', @@ -40,7 +42,8 @@ export const getUserAnalytics = async ( token: string = '', startDate: number | null = null, endDate: number | null = null, - limit: number = 50 + limit: number = 50, + groupId: string | null = null ) => { let error = null; @@ -48,6 +51,7 @@ export const getUserAnalytics = async ( if (startDate) searchParams.append('start_date', startDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString()); if (limit) searchParams.append('limit', limit.toString()); + if (groupId) searchParams.append('group_id', groupId); const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/users?${searchParams.toString()}`, { method: 'GET', @@ -123,13 +127,15 @@ export const getMessages = async ( export const getSummary = async ( token: string = '', startDate: number | null = null, - endDate: number | null = null + endDate: number | null = null, + groupId: string | null = null ) => { let error = null; const searchParams = new URLSearchParams(); if (startDate) searchParams.append('start_date', startDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString()); + if (groupId) searchParams.append('group_id', groupId); const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/summary?${searchParams.toString()}`, { method: 'GET', @@ -160,7 +166,8 @@ export const getDailyStats = async ( token: string = '', startDate: number | null = null, endDate: number | null = null, - granularity: 'hourly' | 'daily' = 'daily' + granularity: 'hourly' | 'daily' = 'daily', + groupId: string | null = null ) => { let error = null; @@ -168,6 +175,7 @@ export const getDailyStats = async ( if (startDate) searchParams.append('start_date', startDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString()); searchParams.append('granularity', granularity); + if (groupId) searchParams.append('group_id', groupId); const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, { method: 'GET', @@ -197,13 +205,15 @@ export const getDailyStats = async ( export const getTokenUsage = async ( token: string = '', startDate: number | null = null, - endDate: number | null = null + endDate: number | null = null, + groupId: string | null = null ) => { let error = null; const searchParams = new URLSearchParams(); if (startDate) searchParams.append('start_date', startDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString()); + if (groupId) searchParams.append('group_id', groupId); const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/tokens?${searchParams.toString()}`, { method: 'GET', diff --git a/src/lib/components/admin/Analytics/Dashboard.svelte b/src/lib/components/admin/Analytics/Dashboard.svelte index c84dfc683..938876163 100644 --- a/src/lib/components/admin/Analytics/Dashboard.svelte +++ b/src/lib/components/admin/Analytics/Dashboard.svelte @@ -2,6 +2,7 @@ import { onMount, getContext } from 'svelte'; import { models } from '$lib/stores'; import { getSummary, getModelAnalytics, getUserAnalytics, getDailyStats, getTokenUsage } from '$lib/apis/analytics'; + import { getGroups } from '$lib/apis/groups'; import Spinner from '$lib/components/common/Spinner.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; @@ -24,6 +25,10 @@ { value: 'all', label: 'All time' } ]; + // User group filter + let groups: Array<{ id: string; name: string }> = []; + let selectedGroupId: string | null = null; + const getDateRange = (period: string): { start: number | null; end: number | null } => { const now = Math.floor(Date.now() / 1000); const day = 86400; @@ -80,11 +85,11 @@ const { start, end } = getDateRange(selectedPeriod); const granularity = selectedPeriod === '24h' ? 'hourly' : 'daily'; const [summaryRes, modelsRes, usersRes, dailyRes, tokensRes] = await Promise.all([ - getSummary(localStorage.token, start, end), - getModelAnalytics(localStorage.token, start, end), - getUserAnalytics(localStorage.token, start, end, 50), - getDailyStats(localStorage.token, start, end, granularity), - getTokenUsage(localStorage.token, start, end) + getSummary(localStorage.token, start, end, selectedGroupId), + getModelAnalytics(localStorage.token, start, end, selectedGroupId), + getUserAnalytics(localStorage.token, start, end, 50, selectedGroupId), + getDailyStats(localStorage.token, start, end, granularity, selectedGroupId), + getTokenUsage(localStorage.token, start, end, selectedGroupId) ]); summary = summaryRes ?? summary; @@ -120,10 +125,20 @@ loading = false; }; - $: if (selectedPeriod) { + $: if (selectedPeriod || selectedGroupId !== undefined) { loadDashboard(); } + onMount(async () => { + // Load groups for filter + try { + const res = await getGroups(localStorage.token); + groups = res ?? []; + } catch (e) { + console.error('Failed to load groups:', e); + } + }); + $: sortedModels = [...modelStats].sort((a, b) => { if (modelOrderBy === 'name') { return modelDirection === 'asc' @@ -159,14 +174,27 @@