enh: analytics
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 @@
|
||||
<div class="text-lg font-medium px-0.5">
|
||||
{$i18n.t('Analytics')}
|
||||
</div>
|
||||
<select
|
||||
bind:value={selectedPeriod}
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
|
||||
>
|
||||
{#each periods as period}
|
||||
<option value={period.value}>{$i18n.t(period.label)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if groups.length > 0}
|
||||
<select
|
||||
bind:value={selectedGroupId}
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
|
||||
>
|
||||
<option value={null}>{$i18n.t('All Users')}</option>
|
||||
{#each groups as group}
|
||||
<option value={group.id}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<select
|
||||
bind:value={selectedPeriod}
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
|
||||
>
|
||||
{#each periods as period}
|
||||
<option value={period.value}>{$i18n.t(period.label)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Details Modal -->
|
||||
|
||||
Reference in New Issue
Block a user