Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions messages/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"filters": {
"user": "User",
"provider": "Provider",
"searchUser": "Search users...",
"searchProvider": "Search providers...",
"noUserFound": "No matching users found",
"noProviderFound": "No matching providers found",
"model": "Model",
"endpoint": "Endpoint",
"status": "Status",
Expand Down
4 changes: 4 additions & 0 deletions messages/ja/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"filters": {
"user": "ユーザー",
"provider": "プロバイダー",
"searchUser": "ユーザーを検索...",
"searchProvider": "プロバイダーを検索...",
"noUserFound": "一致するユーザーが見つかりません",
"noProviderFound": "一致するプロバイダーが見つかりません",
"model": "モデル",
"endpoint": "エンドポイント",
"status": "ステータス",
Expand Down
4 changes: 4 additions & 0 deletions messages/ru/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"filters": {
"user": "Пользователь",
"provider": "Поставщик",
"searchUser": "Поиск пользователей...",
"searchProvider": "Поиск провайдеров...",
"noUserFound": "Пользователи не найдены",
"noProviderFound": "Провайдеры не найдены",
"model": "Модель",
"endpoint": "Эндпоинт",
"status": "Статус",
Expand Down
4 changes: 4 additions & 0 deletions messages/zh-CN/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"filters": {
"user": "用户",
"provider": "供应商",
"searchUser": "搜索用户...",
"searchProvider": "搜索供应商...",
"noUserFound": "未找到匹配的用户",
"noProviderFound": "未找到匹配的供应商",
"model": "模型",
"endpoint": "端点",
"status": "状态",
Expand Down
4 changes: 4 additions & 0 deletions messages/zh-TW/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"filters": {
"user": "使用者",
"provider": "供應商",
"searchUser": "搜尋使用者...",
"searchProvider": "搜尋供應商...",
"noUserFound": "未找到匹配的使用者",
"noProviderFound": "未找到匹配的供應商",
"model": "模型",
"endpoint": "端點",
"status": "狀態",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export function KeyEditSection({
if (!normalizedKeyProviderGroup) return [];
return normalizedKeyProviderGroup.split(",").filter(Boolean);
}, [normalizedKeyProviderGroup]);
const extraKeyGroupOption = useMemo(() => {
const _extraKeyGroupOption = useMemo(() => {
if (!normalizedKeyProviderGroup) return null;
if (normalizedKeyProviderGroup === normalizedUserProviderGroup) return null;
if (userGroups.includes(normalizedKeyProviderGroup)) return null;
Expand Down
200 changes: 154 additions & 46 deletions src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
"use client";

import { addDays, format, parse } from "date-fns";
import { Download } from "lucide-react";
import { Check, ChevronsUpDown, Download } from "lucide-react";
import { useTranslations } from "next-intl";

import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { getKeys } from "@/actions/keys";
import { exportUsageLogs } from "@/actions/usage-logs";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
Select,
SelectContent,
Expand Down Expand Up @@ -99,6 +108,8 @@ export function UsageLogsFilters({
const [keys, setKeys] = useState<Key[]>(initialKeys);
const [localFilters, setLocalFilters] = useState(filters);
const [isExporting, setIsExporting] = useState(false);
const [userPopoverOpen, setUserPopoverOpen] = useState(false);
const [providerPopoverOpen, setProviderPopoverOpen] = useState(false);

useEffect(() => {
if (initialKeys.length > 0) {
Expand Down Expand Up @@ -263,26 +274,72 @@ export function UsageLogsFilters({
{isAdmin && (
<div className="space-y-2 lg:col-span-4">
<Label>{t("logs.filters.user")}</Label>
<Select
value={localFilters.userId?.toString() || ""}
onValueChange={handleUserChange}
disabled={isUsersLoading}
>
<SelectTrigger>
<SelectValue
placeholder={
isUsersLoading ? t("logs.stats.loading") : t("logs.filters.allUsers")
}
/>
</SelectTrigger>
<SelectContent>
{users.map((user) => (
<SelectItem key={user.id} value={user.id.toString()}>
{user.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={userPopoverOpen}
disabled={isUsersLoading}
type="button"
className="w-full justify-between"
>
{localFilters.userId ? (
(users.find((user) => user.id === localFilters.userId)?.name ??
localFilters.userId.toString())
) : (
<span className="text-muted-foreground">
{isUsersLoading ? t("logs.stats.loading") : t("logs.filters.allUsers")}
</span>
)}
Comment on lines +287 to +294
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation uses users.find() to get the selected user's name on every render. This can be inefficient for large lists of users, which is the scenario this PR addresses. A linear search on every render could lead to performance issues.

To optimize this, you can create a Map of user IDs to names using useMemo. This will provide a more performant O(1) lookup.

Add this to your component:

const userMap = useMemo(() => new Map(users.map((user) => [user.id, user.name])), [users]);
Suggested change
{localFilters.userId ? (
(users.find((user) => user.id === localFilters.userId)?.name ??
localFilters.userId.toString())
) : (
<span className="text-muted-foreground">
{isUsersLoading ? t("logs.stats.loading") : t("logs.filters.allUsers")}
</span>
)}
{localFilters.userId ? (
userMap.get(localFilters.userId) ?? localFilters.userId.toString()
) : (
<span className="text-muted-foreground">
{isUsersLoading ? t("logs.stats.loading") : t("logs.filters.allUsers")}
</span>
)}

<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[320px] p-0"
align="start"
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
<Command shouldFilter={true}>
<CommandInput placeholder={t("logs.filters.searchUser")} />
<CommandList className="max-h-[250px] overflow-y-auto">
<CommandEmpty>
{isUsersLoading ? t("logs.stats.loading") : t("logs.filters.noUserFound")}
</CommandEmpty>
<CommandGroup>
<CommandItem
value={t("logs.filters.allUsers")}
onSelect={() => {
void handleUserChange("");
setUserPopoverOpen(false);
}}
className="cursor-pointer"
>
<span className="flex-1">{t("logs.filters.allUsers")}</span>
{!localFilters.userId && <Check className="h-4 w-4 text-primary" />}
</CommandItem>
{users.map((user) => (
<CommandItem
key={user.id}
value={user.name}
onSelect={() => {
void handleUserChange(user.id.toString());
setUserPopoverOpen(false);
}}
className="cursor-pointer"
>
<span className="flex-1">{user.name}</span>
{localFilters.userId === user.id && (
<Check className="h-4 w-4 text-primary" />
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)}

Expand Down Expand Up @@ -324,31 +381,82 @@ export function UsageLogsFilters({
{isAdmin && (
<div className="space-y-2 lg:col-span-4">
<Label>{t("logs.filters.provider")}</Label>
<Select
value={localFilters.providerId?.toString() || ""}
onValueChange={(value: string) =>
setLocalFilters({
...localFilters,
providerId: value ? parseInt(value, 10) : undefined,
})
}
disabled={isProvidersLoading}
>
<SelectTrigger>
<SelectValue
placeholder={
isProvidersLoading ? t("logs.stats.loading") : t("logs.filters.allProviders")
}
/>
</SelectTrigger>
<SelectContent>
{providers.map((provider) => (
<SelectItem key={provider.id} value={provider.id.toString()}>
{provider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Popover open={providerPopoverOpen} onOpenChange={setProviderPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={providerPopoverOpen}
disabled={isProvidersLoading}
type="button"
className="w-full justify-between"
>
{localFilters.providerId ? (
(providers.find((provider) => provider.id === localFilters.providerId)?.name ??
localFilters.providerId.toString())
) : (
<span className="text-muted-foreground">
{isProvidersLoading
? t("logs.stats.loading")
: t("logs.filters.allProviders")}
</span>
)}
Comment on lines +394 to +403
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the user filter, providers.find() is used here on every render, which can be inefficient for a large number of providers. This can be optimized by using a Map for O(1) lookups.

Add this to your component:

const providerMap = useMemo(() => new Map(providers.map((provider) => [provider.id, provider.name])), [providers]);
Suggested change
{localFilters.providerId ? (
(providers.find((provider) => provider.id === localFilters.providerId)?.name ??
localFilters.providerId.toString())
) : (
<span className="text-muted-foreground">
{isProvidersLoading
? t("logs.stats.loading")
: t("logs.filters.allProviders")}
</span>
)}
{localFilters.providerId ? (
providerMap.get(localFilters.providerId) ??
localFilters.providerId.toString()
) : (
<span className="text-muted-foreground">
{isProvidersLoading
? t("logs.stats.loading")
: t("logs.filters.allProviders")}
</span>
)}

<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[320px] p-0"
align="start"
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
<Command shouldFilter={true}>
<CommandInput placeholder={t("logs.filters.searchProvider")} />
<CommandList className="max-h-[250px] overflow-y-auto">
<CommandEmpty>
{isProvidersLoading
? t("logs.stats.loading")
: t("logs.filters.noProviderFound")}
</CommandEmpty>
<CommandGroup>
<CommandItem
value={t("logs.filters.allProviders")}
onSelect={() => {
setLocalFilters({
...localFilters,
providerId: undefined,
});
setProviderPopoverOpen(false);
}}
className="cursor-pointer"
>
<span className="flex-1">{t("logs.filters.allProviders")}</span>
{!localFilters.providerId && <Check className="h-4 w-4 text-primary" />}
</CommandItem>
{providers.map((provider) => (
<CommandItem
key={provider.id}
value={provider.name}
onSelect={() => {
setLocalFilters({
...localFilters,
providerId: provider.id,
});
setProviderPopoverOpen(false);
}}
className="cursor-pointer"
>
<span className="flex-1">{provider.name}</span>
{localFilters.providerId === provider.id && (
<Check className="h-4 w-4 text-primary" />
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)}

Expand Down
8 changes: 3 additions & 5 deletions src/repository/usage-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,8 @@ export async function findUsageLogsBatch(
// Cursor-based pagination: WHERE (created_at, id) < (cursor_created_at, cursor_id)
// Using row value comparison for efficient keyset pagination
if (cursor) {
const cursorDate = new Date(cursor.createdAt);
conditions.push(
sql`(${messageRequest.createdAt}, ${messageRequest.id}) < (${cursorDate.toISOString()}::timestamptz, ${cursor.id})`
sql`(${messageRequest.createdAt}, ${messageRequest.id}) < (${cursor.createdAt}::timestamptz, ${cursor.id})`
);
}

Expand All @@ -185,6 +184,7 @@ export async function findUsageLogsBatch(
.select({
id: messageRequest.id,
createdAt: messageRequest.createdAt,
createdAtRaw: sql<string>`to_char(${messageRequest.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] [LOGIC-BUG] createdAtRaw formats timestamptz in session timezone but appends Z (UTC), so nextCursor can point at the wrong rows when DB TimeZone ≠ UTC.

Evidence (src/repository/usage-logs.ts:187):
createdAtRaw: sql<string>\to_char(${messageRequest.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`,`

Why this is a problem: to_char(timestamptz, ...) uses the connection/session TimeZone to render the timestamp, but the format hardcodes "Z". In non-UTC deployments this produces a cursor string that is not the same instant, and (${messageRequest.createdAt}, ${messageRequest.id}) < (${cursor.createdAt}::timestamptz, ...) can skip/duplicate log rows.

Suggested fix:

createdAtRaw: sql<string>`
  to_char(
    ${messageRequest.createdAt} AT TIME ZONE 'UTC',
    'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
  )
`,

sessionId: messageRequest.sessionId,
requestSequence: messageRequest.requestSequence,
userName: users.name,
Expand Down Expand Up @@ -228,9 +228,7 @@ export async function findUsageLogsBatch(
// Calculate next cursor from the last record
const lastLog = logsToReturn[logsToReturn.length - 1];
const nextCursor =
hasMore && lastLog?.createdAt
? { createdAt: lastLog.createdAt.toISOString(), id: lastLog.id }
: null;
hasMore && lastLog?.createdAtRaw ? { createdAt: lastLog.createdAtRaw, id: lastLog.id } : null;

const logs: UsageLogRow[] = logsToReturn.map((row) => {
const totalRowTokens =
Expand Down
8 changes: 4 additions & 4 deletions src/repository/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,8 @@ export async function findUserListBatch(

// Cursor-based pagination: WHERE (created_at, id) > (cursor_created_at, cursor_id)
if (cursor) {
const cursorDate = new Date(cursor.createdAt);
conditions.push(
sql`(${users.createdAt}, ${users.id}) > (${cursorDate.toISOString()}::timestamptz, ${cursor.id})`
sql`(${users.createdAt}, ${users.id}) > (${cursor.createdAt}::timestamptz, ${cursor.id})`
);
}

Expand All @@ -187,6 +186,7 @@ export async function findUserListBatch(
providerGroup: users.providerGroup,
tags: users.tags,
createdAt: users.createdAt,
createdAtRaw: sql<string>`to_char(${users.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] [LOGIC-BUG] createdAtRaw formats timestamptz in session timezone but appends Z (UTC), breaking keyset cursors outside UTC.

Evidence (src/repository/user.ts:189):
createdAtRaw: sql<string>\to_char(${users.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`,`

Why this is a problem: to_char(timestamptz, ...) uses the DB session TimeZone for formatting; the literal "Z" claims the value is UTC. If TimeZone ≠ UTC, the cursor string is shifted, and (${users.createdAt}, ${users.id}) > (${cursor.createdAt}::timestamptz, ...) can skip/duplicate rows.

Suggested fix:

createdAtRaw: sql<string>`
  to_char(${users.createdAt} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')
`,

updatedAt: users.updatedAt,
deletedAt: users.deletedAt,
limit5hUsd: users.limit5hUsd,
Expand All @@ -211,8 +211,8 @@ export async function findUserListBatch(

const lastUser = usersToReturn[usersToReturn.length - 1];
const nextCursor =
hasMore && lastUser?.createdAt
? { createdAt: lastUser.createdAt.toISOString(), id: lastUser.id }
hasMore && lastUser?.createdAtRaw
? { createdAt: lastUser.createdAtRaw, id: lastUser.id }
: null;

return {
Expand Down
Loading