Skip to content

Commit e62649f

Browse files
committed
enh: analytics
1 parent 0e60c75 commit e62649f

4 files changed

Lines changed: 95 additions & 28 deletions

File tree

backend/open_webui/models/chat_messages.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,12 @@ def get_message_count_by_model(
312312
self,
313313
start_date: Optional[int] = None,
314314
end_date: Optional[int] = None,
315+
group_id: Optional[str] = None,
315316
db: Optional[Session] = None,
316317
) -> dict[str, int]:
317318
with get_db_context(db) as db:
318319
from sqlalchemy import func
320+
from open_webui.models.groups import GroupMember
319321

320322
query = db.query(
321323
ChatMessage.model_id, func.count(ChatMessage.id).label("count")
@@ -329,6 +331,9 @@ def get_message_count_by_model(
329331
query = query.filter(ChatMessage.created_at >= start_date)
330332
if end_date:
331333
query = query.filter(ChatMessage.created_at <= end_date)
334+
if group_id:
335+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
336+
query = query.filter(ChatMessage.user_id.in_(group_users))
332337

333338
results = query.group_by(ChatMessage.model_id).all()
334339
return {row.model_id: row.count for row in results}
@@ -337,11 +342,13 @@ def get_token_usage_by_model(
337342
self,
338343
start_date: Optional[int] = None,
339344
end_date: Optional[int] = None,
345+
group_id: Optional[str] = None,
340346
db: Optional[Session] = None,
341347
) -> dict[str, dict]:
342348
"""Aggregate token usage by model using database-level aggregation."""
343349
with get_db_context(db) as db:
344350
from sqlalchemy import func, cast, Integer
351+
from open_webui.models.groups import GroupMember
345352

346353
dialect = db.bind.dialect.name
347354

@@ -379,6 +386,9 @@ def get_token_usage_by_model(
379386
query = query.filter(ChatMessage.created_at >= start_date)
380387
if end_date:
381388
query = query.filter(ChatMessage.created_at <= end_date)
389+
if group_id:
390+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
391+
query = query.filter(ChatMessage.user_id.in_(group_users))
382392

383393
results = query.group_by(ChatMessage.model_id).all()
384394

@@ -455,10 +465,12 @@ def get_message_count_by_user(
455465
self,
456466
start_date: Optional[int] = None,
457467
end_date: Optional[int] = None,
468+
group_id: Optional[str] = None,
458469
db: Optional[Session] = None,
459470
) -> dict[str, int]:
460471
with get_db_context(db) as db:
461472
from sqlalchemy import func
473+
from open_webui.models.groups import GroupMember
462474

463475
query = db.query(
464476
ChatMessage.user_id, func.count(ChatMessage.id).label("count")
@@ -468,6 +480,9 @@ def get_message_count_by_user(
468480
query = query.filter(ChatMessage.created_at >= start_date)
469481
if end_date:
470482
query = query.filter(ChatMessage.created_at <= end_date)
483+
if group_id:
484+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
485+
query = query.filter(ChatMessage.user_id.in_(group_users))
471486

472487
results = query.group_by(ChatMessage.user_id).all()
473488
return {row.user_id: row.count for row in results}
@@ -476,10 +491,12 @@ def get_message_count_by_chat(
476491
self,
477492
start_date: Optional[int] = None,
478493
end_date: Optional[int] = None,
494+
group_id: Optional[str] = None,
479495
db: Optional[Session] = None,
480496
) -> dict[str, int]:
481497
with get_db_context(db) as db:
482498
from sqlalchemy import func
499+
from open_webui.models.groups import GroupMember
483500

484501
query = db.query(
485502
ChatMessage.chat_id, func.count(ChatMessage.id).label("count")
@@ -489,6 +506,9 @@ def get_message_count_by_chat(
489506
query = query.filter(ChatMessage.created_at >= start_date)
490507
if end_date:
491508
query = query.filter(ChatMessage.created_at <= end_date)
509+
if group_id:
510+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
511+
query = query.filter(ChatMessage.user_id.in_(group_users))
492512

493513
results = query.group_by(ChatMessage.chat_id).all()
494514
return {row.chat_id: row.count for row in results}
@@ -497,11 +517,13 @@ def get_daily_message_counts_by_model(
497517
self,
498518
start_date: Optional[int] = None,
499519
end_date: Optional[int] = None,
520+
group_id: Optional[str] = None,
500521
db: Optional[Session] = None,
501522
) -> dict[str, dict[str, int]]:
502523
"""Get message counts grouped by day and model."""
503524
with get_db_context(db) as db:
504525
from datetime import datetime, timedelta
526+
from open_webui.models.groups import GroupMember
505527

506528
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
507529
ChatMessage.role == "assistant",
@@ -513,6 +535,9 @@ def get_daily_message_counts_by_model(
513535
query = query.filter(ChatMessage.created_at >= start_date)
514536
if end_date:
515537
query = query.filter(ChatMessage.created_at <= end_date)
538+
if group_id:
539+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
540+
query = query.filter(ChatMessage.user_id.in_(group_users))
516541

517542
results = query.all()
518543

backend/open_webui/routers/analytics.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from open_webui.models.chat_messages import ChatMessages, ChatMessageModel
99
from open_webui.models.chats import Chats
10+
from open_webui.models.groups import Groups
1011
from open_webui.models.users import Users
1112
from open_webui.models.feedbacks import Feedbacks
1213
from open_webui.utils.auth import get_admin_user
@@ -56,12 +57,13 @@ class UserAnalyticsResponse(BaseModel):
5657
async def get_model_analytics(
5758
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
5859
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
60+
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
5961
user=Depends(get_admin_user),
6062
db: Session = Depends(get_session),
6163
):
6264
"""Get message counts per model."""
6365
counts = ChatMessages.get_message_count_by_model(
64-
start_date=start_date, end_date=end_date, db=db
66+
start_date=start_date, end_date=end_date, group_id=group_id, db=db
6567
)
6668
models = [
6769
ModelAnalyticsEntry(model_id=model_id, count=count)
@@ -74,15 +76,14 @@ async def get_model_analytics(
7476
async def get_user_analytics(
7577
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
7678
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
79+
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
7780
limit: int = Query(50, description="Max users to return"),
7881
user=Depends(get_admin_user),
7982
db: Session = Depends(get_session),
8083
):
8184
"""Get message counts and token usage per user with user info."""
82-
from open_webui.models.users import Users
83-
8485
counts = ChatMessages.get_message_count_by_user(
85-
start_date=start_date, end_date=end_date, db=db
86+
start_date=start_date, end_date=end_date, group_id=group_id, db=db
8687
)
8788
token_usage = ChatMessages.get_token_usage_by_user(
8889
start_date=start_date, end_date=end_date, db=db
@@ -153,18 +154,19 @@ class SummaryResponse(BaseModel):
153154
async def get_summary(
154155
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
155156
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
157+
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
156158
user=Depends(get_admin_user),
157159
db: Session = Depends(get_session),
158160
):
159161
"""Get summary statistics for the dashboard."""
160162
model_counts = ChatMessages.get_message_count_by_model(
161-
start_date=start_date, end_date=end_date, db=db
163+
start_date=start_date, end_date=end_date, group_id=group_id, db=db
162164
)
163165
user_counts = ChatMessages.get_message_count_by_user(
164-
start_date=start_date, end_date=end_date, db=db
166+
start_date=start_date, end_date=end_date, group_id=group_id, db=db
165167
)
166168
chat_counts = ChatMessages.get_message_count_by_chat(
167-
start_date=start_date, end_date=end_date, db=db
169+
start_date=start_date, end_date=end_date, group_id=group_id, db=db
168170
)
169171

170172
return SummaryResponse(
@@ -188,6 +190,7 @@ class DailyStatsResponse(BaseModel):
188190
async def get_daily_stats(
189191
start_date: Optional[int] = Query(None, description="Start timestamp (epoch)"),
190192
end_date: Optional[int] = Query(None, description="End timestamp (epoch)"),
193+
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
191194
granularity: str = Query("daily", description="Granularity: 'hourly' or 'daily'"),
192195
user=Depends(get_admin_user),
193196
db: Session = Depends(get_session),
@@ -199,7 +202,7 @@ async def get_daily_stats(
199202
)
200203
else:
201204
counts = ChatMessages.get_daily_message_counts_by_model(
202-
start_date=start_date, end_date=end_date, db=db
205+
start_date=start_date, end_date=end_date, group_id=group_id, db=db
203206
)
204207
return DailyStatsResponse(
205208
data=[
@@ -228,12 +231,13 @@ class TokenUsageResponse(BaseModel):
228231
async def get_token_usage(
229232
start_date: Optional[int] = Query(None),
230233
end_date: Optional[int] = Query(None),
234+
group_id: Optional[str] = Query(None, description="Filter by user group ID"),
231235
user=Depends(get_admin_user),
232236
db: Session = Depends(get_session),
233237
):
234238
"""Get token usage aggregated by model."""
235239
usage = ChatMessages.get_token_usage_by_model(
236-
start_date=start_date, end_date=end_date, db=db
240+
start_date=start_date, end_date=end_date, group_id=group_id, db=db
237241
)
238242

239243
models = [

src/lib/apis/analytics/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { WEBUI_API_BASE_URL } from '$lib/constants';
33
export const getModelAnalytics = async (
44
token: string = '',
55
startDate: number | null = null,
6-
endDate: number | null = null
6+
endDate: number | null = null,
7+
groupId: string | null = null
78
) => {
89
let error = null;
910

1011
const searchParams = new URLSearchParams();
1112
if (startDate) searchParams.append('start_date', startDate.toString());
1213
if (endDate) searchParams.append('end_date', endDate.toString());
14+
if (groupId) searchParams.append('group_id', groupId);
1315

1416
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models?${searchParams.toString()}`, {
1517
method: 'GET',
@@ -40,14 +42,16 @@ export const getUserAnalytics = async (
4042
token: string = '',
4143
startDate: number | null = null,
4244
endDate: number | null = null,
43-
limit: number = 50
45+
limit: number = 50,
46+
groupId: string | null = null
4447
) => {
4548
let error = null;
4649

4750
const searchParams = new URLSearchParams();
4851
if (startDate) searchParams.append('start_date', startDate.toString());
4952
if (endDate) searchParams.append('end_date', endDate.toString());
5053
if (limit) searchParams.append('limit', limit.toString());
54+
if (groupId) searchParams.append('group_id', groupId);
5155

5256
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/users?${searchParams.toString()}`, {
5357
method: 'GET',
@@ -123,13 +127,15 @@ export const getMessages = async (
123127
export const getSummary = async (
124128
token: string = '',
125129
startDate: number | null = null,
126-
endDate: number | null = null
130+
endDate: number | null = null,
131+
groupId: string | null = null
127132
) => {
128133
let error = null;
129134

130135
const searchParams = new URLSearchParams();
131136
if (startDate) searchParams.append('start_date', startDate.toString());
132137
if (endDate) searchParams.append('end_date', endDate.toString());
138+
if (groupId) searchParams.append('group_id', groupId);
133139

134140
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/summary?${searchParams.toString()}`, {
135141
method: 'GET',
@@ -160,14 +166,16 @@ export const getDailyStats = async (
160166
token: string = '',
161167
startDate: number | null = null,
162168
endDate: number | null = null,
163-
granularity: 'hourly' | 'daily' = 'daily'
169+
granularity: 'hourly' | 'daily' = 'daily',
170+
groupId: string | null = null
164171
) => {
165172
let error = null;
166173

167174
const searchParams = new URLSearchParams();
168175
if (startDate) searchParams.append('start_date', startDate.toString());
169176
if (endDate) searchParams.append('end_date', endDate.toString());
170177
searchParams.append('granularity', granularity);
178+
if (groupId) searchParams.append('group_id', groupId);
171179

172180
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, {
173181
method: 'GET',
@@ -197,13 +205,15 @@ export const getDailyStats = async (
197205
export const getTokenUsage = async (
198206
token: string = '',
199207
startDate: number | null = null,
200-
endDate: number | null = null
208+
endDate: number | null = null,
209+
groupId: string | null = null
201210
) => {
202211
let error = null;
203212

204213
const searchParams = new URLSearchParams();
205214
if (startDate) searchParams.append('start_date', startDate.toString());
206215
if (endDate) searchParams.append('end_date', endDate.toString());
216+
if (groupId) searchParams.append('group_id', groupId);
207217

208218
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/tokens?${searchParams.toString()}`, {
209219
method: 'GET',

src/lib/components/admin/Analytics/Dashboard.svelte

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { onMount, getContext } from 'svelte';
33
import { models } from '$lib/stores';
44
import { getSummary, getModelAnalytics, getUserAnalytics, getDailyStats, getTokenUsage } from '$lib/apis/analytics';
5+
import { getGroups } from '$lib/apis/groups';
56
import Spinner from '$lib/components/common/Spinner.svelte';
67
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
78
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
@@ -24,6 +25,10 @@
2425
{ value: 'all', label: 'All time' }
2526
];
2627
28+
// User group filter
29+
let groups: Array<{ id: string; name: string }> = [];
30+
let selectedGroupId: string | null = null;
31+
2732
const getDateRange = (period: string): { start: number | null; end: number | null } => {
2833
const now = Math.floor(Date.now() / 1000);
2934
const day = 86400;
@@ -80,11 +85,11 @@
8085
const { start, end } = getDateRange(selectedPeriod);
8186
const granularity = selectedPeriod === '24h' ? 'hourly' : 'daily';
8287
const [summaryRes, modelsRes, usersRes, dailyRes, tokensRes] = await Promise.all([
83-
getSummary(localStorage.token, start, end),
84-
getModelAnalytics(localStorage.token, start, end),
85-
getUserAnalytics(localStorage.token, start, end, 50),
86-
getDailyStats(localStorage.token, start, end, granularity),
87-
getTokenUsage(localStorage.token, start, end)
88+
getSummary(localStorage.token, start, end, selectedGroupId),
89+
getModelAnalytics(localStorage.token, start, end, selectedGroupId),
90+
getUserAnalytics(localStorage.token, start, end, 50, selectedGroupId),
91+
getDailyStats(localStorage.token, start, end, granularity, selectedGroupId),
92+
getTokenUsage(localStorage.token, start, end, selectedGroupId)
8893
]);
8994
9095
summary = summaryRes ?? summary;
@@ -120,10 +125,20 @@
120125
loading = false;
121126
};
122127
123-
$: if (selectedPeriod) {
128+
$: if (selectedPeriod || selectedGroupId !== undefined) {
124129
loadDashboard();
125130
}
126131
132+
onMount(async () => {
133+
// Load groups for filter
134+
try {
135+
const res = await getGroups(localStorage.token);
136+
groups = res ?? [];
137+
} catch (e) {
138+
console.error('Failed to load groups:', e);
139+
}
140+
});
141+
127142
$: sortedModels = [...modelStats].sort((a, b) => {
128143
if (modelOrderBy === 'name') {
129144
return modelDirection === 'asc'
@@ -159,14 +174,27 @@
159174
<div class="text-lg font-medium px-0.5">
160175
{$i18n.t('Analytics')}
161176
</div>
162-
<select
163-
bind:value={selectedPeriod}
164-
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
165-
>
166-
{#each periods as period}
167-
<option value={period.value}>{$i18n.t(period.label)}</option>
168-
{/each}
169-
</select>
177+
<div class="flex items-center gap-2">
178+
{#if groups.length > 0}
179+
<select
180+
bind:value={selectedGroupId}
181+
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
182+
>
183+
<option value={null}>{$i18n.t('All Users')}</option>
184+
{#each groups as group}
185+
<option value={group.id}>{group.name}</option>
186+
{/each}
187+
</select>
188+
{/if}
189+
<select
190+
bind:value={selectedPeriod}
191+
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-none text-right"
192+
>
193+
{#each periods as period}
194+
<option value={period.value}>{$i18n.t(period.label)}</option>
195+
{/each}
196+
</select>
197+
</div>
170198
</div>
171199

172200
<!-- Model Details Modal -->

0 commit comments

Comments
 (0)