-
Notifications
You must be signed in to change notification settings - Fork 142
feat: Dashboard Logs:秒级时间筛选 + Session ID 精确筛选/联想/展示(含回归修复) #611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Dashboard Logs:秒级时间筛选 + Session ID 精确筛选/联想/展示(含回归修复) #611
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthrough为 Dashboard Usage Logs 引入 Session ID 支持:添加 UI 文案与组件(自动补全、复制)、URL 查询与时间范围工具、后端 repository/动作支持、数据库前缀索引、剪贴板与 LIKE 工具、公测配置与大量单元测试。 Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @YangQing-Lin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 本次拉取请求旨在全面增强 Dashboard Logs 页面的用户体验和功能性。通过引入秒级时间筛选和 Session ID 精确筛选及联想功能,用户现在可以更精细、高效地定位和分析日志数据。此外,PR 还解决了在近期代码审查中发现的关键回归问题,提升了系统的健壮性和可用性,确保了筛选逻辑的准确性和一致性。 Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
本 PR 实现了仪表盘日志页面的多项重要功能增强,包括秒级时间筛选和 Session ID 的精确筛选与联想功能,并修复了分页参数泄漏和联想请求去重等关键回归问题。整体实现质量很高,代码结构清晰,特别是新增了 URL 参数解析和构建的统一工具函数,以及详尽的单元测试和专项覆盖率配置,显著提升了代码的可维护性和健壮性。
我提出了一些建议,主要集中在代码复用、性能优化和可维护性方面,希望能帮助进一步提升代码质量。
src/repository/usage-logs.ts
Outdated
| EXCLUDE_WARMUP_CONDITION, | ||
| sql`${messageRequest.sessionId} IS NOT NULL`, | ||
| sql`length(${messageRequest.sessionId}) > 0`, | ||
| sql`${messageRequest.sessionId} ILIKE ${`%${trimmedTerm}%`}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
正如 PR 描述中提到的,使用 ILIKE '%...%' 进行模糊搜索在数据量大时可能导致严重的性能问题,因为它会触发全表扫描。虽然本次提交通过去抖和长度限制等方式进行了一定缓解,但我们应考虑一个更具扩展性的长期解决方案。可行的选项包括:
- 使用 trigram 索引(例如
CREATE INDEX ON message_request USING GIN (session_id gin_trgm_ops);),这可以显著加速ILIKE查询。 - 将搜索限制为仅从字符串开头匹配(
ILIKE '${trimmedTerm}%'),这样可以利用标准的 B-tree 索引。 - 为此功能使用专门的搜索引擎,如 Elasticsearch。
考虑到潜在的影响,添加 trigram 索引似乎是很好的下一步。
|
|
||
| // 硬编码常用状态码(首次渲染时显示,无需等待加载) | ||
| const COMMON_STATUS_CODES: number[] = [200, 400, 401, 429, 500]; | ||
| const SESSION_ID_SUGGESTION_MIN_LEN = 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const filters = useMemo<VirtualizedLogsTableFilters & { page?: number }>(() => { | ||
| const parsed = parseLogsUrlFilters({ | ||
| userId: searchParams.userId, | ||
| keyId: searchParams.keyId, | ||
| providerId: searchParams.providerId, | ||
| sessionId: searchParams.sessionId, | ||
| startTime: searchParams.startTime, | ||
| endTime: searchParams.endTime, | ||
| statusCode: searchParams.statusCode, | ||
| model: searchParams.model, | ||
| endpoint: searchParams.endpoint, | ||
| minRetry: searchParams.minRetry, | ||
| page: searchParams.page, | ||
| }); | ||
| return { | ||
| userId: parsed.userId, | ||
| keyId: parsed.keyId, | ||
| providerId: parsed.providerId, | ||
| sessionId: parsed.sessionId, | ||
| startTime: parsed.startTime, | ||
| endTime: parsed.endTime, | ||
| statusCode: parsed.statusCode, | ||
| excludeStatusCode200: parsed.excludeStatusCode200, | ||
| model: parsed.model, | ||
| endpoint: parsed.endpoint, | ||
| minRetryCount: parsed.minRetryCount, | ||
| page: parsed.page, | ||
| }; | ||
| }, [ | ||
| searchParams.userId, | ||
| searchParams.keyId, | ||
| searchParams.providerId, | ||
| searchParams.sessionId, | ||
| searchParams.startTime, | ||
| searchParams.endTime, | ||
| searchParams.statusCode, | ||
| searchParams.model, | ||
| searchParams.endpoint, | ||
| searchParams.minRetry, | ||
| searchParams.page, | ||
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
用于解析筛选器的 useMemo hook 可以被简化。parseLogsUrlFilters 函数已经返回了具有所需结构的对象,因此手动映射每个属性是多余的,可以直接返回其结果。
const filters = useMemo<VirtualizedLogsTableFilters & { page?: number }>(() =>
parseLogsUrlFilters({
userId: searchParams.userId,
keyId: searchParams.keyId,
providerId: searchParams.providerId,
sessionId: searchParams.sessionId,
startTime: searchParams.startTime,
endTime: searchParams.endTime,
statusCode: searchParams.statusCode,
model: searchParams.model,
endpoint: searchParams.endpoint,
minRetry: searchParams.minRetry,
page: searchParams.page,
}),
[
searchParams.userId,
searchParams.keyId,
searchParams.providerId,
searchParams.sessionId,
searchParams.startTime,
searchParams.endTime,
searchParams.statusCode,
searchParams.model,
searchParams.endpoint,
searchParams.minRetry,
searchParams.page,
]);
| const handleCopySessionIdClick = useCallback( | ||
| (event: MouseEvent<HTMLButtonElement>) => { | ||
| const sessionId = event.currentTarget.dataset.sessionId; | ||
| if (!sessionId) return; | ||
|
|
||
| const clipboard = navigator.clipboard; | ||
| if (clipboard) { | ||
| void clipboard | ||
| .writeText(sessionId) | ||
| .then(() => toast.success(t("actions.copied"))) | ||
| .catch(() => {}); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const textarea = document.createElement("textarea"); | ||
| textarea.value = sessionId; | ||
| textarea.setAttribute("readonly", ""); | ||
| textarea.style.position = "absolute"; | ||
| textarea.style.left = "-9999px"; | ||
| document.body.appendChild(textarea); | ||
| textarea.select(); | ||
| const ok = document.execCommand("copy"); | ||
| document.body.removeChild(textarea); | ||
| if (ok) toast.success(t("actions.copied")); | ||
| } catch {} | ||
| }, | ||
| [t] | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/repository/usage-logs.ts`:
- Around line 578-584: The ILIKE pattern currently interpolates trimmedTerm
directly, allowing literal '%' or '_' in trimmedTerm to act as wildcards; add an
escape function (e.g., escapeLike(s: string)) that escapes %, _, and backslash,
build pattern = `%${escapeLike(trimmedTerm)}%`, and replace the
sql`${messageRequest.sessionId} ILIKE ${`%${trimmedTerm}%`}` entry in the
conditions array with sql`${messageRequest.sessionId} ILIKE ${pattern} ESCAPE
'\\'` so the pattern matches literals correctly and uses the ESCAPE clause.
🧹 Nitpick comments (12)
src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx (1)
194-235: 测试覆盖了剪贴板 API 路径,但未覆盖 fallback 路径。当前测试仅验证了
navigator.clipboard.writeText可用时的情况。建议补充以下测试场景以提高覆盖率:
navigator.clipboard不可用时的execCommand("copy")fallback 路径clipboard.writeText失败时的静默处理此外,
Object.defineProperty(navigator, "clipboard", ...)修改了全局对象,建议在测试后恢复原始值以避免测试间干扰。建议添加 cleanup 和 fallback 测试
test("copies sessionId on click and shows toast", async () => { + const originalClipboard = navigator.clipboard; const writeText = vi.fn(async () => {}); Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true, }); // ... existing test code ... await act(async () => { root.unmount(); }); container.remove(); + + // Restore original clipboard + Object.defineProperty(navigator, "clipboard", { + value: originalClipboard, + configurable: true, + }); }); + + test("uses execCommand fallback when clipboard API unavailable", async () => { + const originalClipboard = navigator.clipboard; + Object.defineProperty(navigator, "clipboard", { + value: undefined, + configurable: true, + }); + const execCommandSpy = vi.spyOn(document, "execCommand").mockReturnValue(true); + + // ... render and click test ... + + expect(execCommandSpy).toHaveBeenCalledWith("copy"); + expect(toastMocks.success).toHaveBeenCalledWith("actions.copied"); + + execCommandSpy.mockRestore(); + Object.defineProperty(navigator, "clipboard", { + value: originalClipboard, + configurable: true, + }); + });src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)
71-99:handleCopySessionIdClick与usage-logs-table.tsx中的实现重复。根据 relevant code snippets,
usage-logs-table.tsx(lines 66-91) 中存在完全相同的复制逻辑。建议将此函数抽取为共享 hook 或工具函数,避免代码重复。此外,Line 96 的空
catch {}会静默吞掉所有错误,建议至少记录到 console 以便调试。建议抽取共享的复制逻辑
创建共享 hook,例如
src/hooks/use-copy-to-clipboard.ts:import { useCallback } from "react"; import { toast } from "sonner"; import { useTranslations } from "next-intl"; export function useCopyToClipboard() { const t = useTranslations("dashboard"); return useCallback(async (text: string) => { if (navigator.clipboard) { try { await navigator.clipboard.writeText(text); toast.success(t("actions.copied")); return; } catch { // Fall through to execCommand fallback } } try { const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.position = "absolute"; textarea.style.left = "-9999px"; document.body.appendChild(textarea); textarea.select(); const ok = document.execCommand("copy"); document.body.removeChild(textarea); if (ok) toast.success(t("actions.copied")); } catch (err) { console.warn("Copy to clipboard failed:", err); } }, [t]); }然后在两个组件中使用:
- const handleCopySessionIdClick = useCallback( - (event: MouseEvent<HTMLButtonElement>) => { - const sessionId = event.currentTarget.dataset.sessionId; - if (!sessionId) return; - // ... duplicated logic ... - }, - [t] - ); + const copyToClipboard = useCopyToClipboard(); + const handleCopySessionIdClick = useCallback( + (event: MouseEvent<HTMLButtonElement>) => { + const sessionId = event.currentTarget.dataset.sessionId; + if (sessionId) void copyToClipboard(sessionId); + }, + [copyToClipboard] + );tests/unit/repository/usage-logs-sessionid-suggestions.test.ts (1)
133-146: 建议补充 limit 下界测试当前测试验证了
limit: 500被 clamp 到 50(上界),但未验证limit: 0或limit: -1被 clamp 到 1(下界)。参考src/repository/usage-logs.ts中的实现Math.min(50, Math.max(1, filters.limit ?? 20)),建议补充下界测试。建议添加下界测试用例
test("limit 应被 clamp 到 [1, 50]", async () => { vi.resetModules(); const limitArgs: unknown[] = []; const selectMock = vi.fn(() => createThenableQuery([], { limitArgs })); vi.doMock("@/drizzle/db", () => ({ db: { select: selectMock }, })); const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs"); await findUsageLogSessionIdSuggestions({ term: "abc", limit: 500 }); expect(limitArgs).toEqual([50]); }); + + test("limit 下界应 clamp 到 1", async () => { + vi.resetModules(); + + const limitArgs: unknown[] = []; + const selectMock = vi.fn(() => createThenableQuery([], { limitArgs })); + vi.doMock("@/drizzle/db", () => ({ + db: { select: selectMock }, + })); + + const { findUsageLogSessionIdSuggestions } = await import("@/repository/usage-logs"); + await findUsageLogSessionIdSuggestions({ term: "abc", limit: 0 }); + + expect(limitArgs).toEqual([1]); + });tests/unit/dashboard-logs-filters-time-range.test.tsx (1)
82-85: 建议使用 data-testid 替代文本匹配当前通过按钮文本
"Apply Filter"查找元素,如果 i18n 翻译变更或组件使用不同 locale 测试,可能导致测试失败。建议在组件中添加data-testid="apply-filter-btn"并使用其定位。- const applyBtn = Array.from(container.querySelectorAll("button")).find( - (b) => (b.textContent || "").trim() === "Apply Filter" - ); + const applyBtn = container.querySelector("[data-testid='apply-filter-btn']");src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx (1)
66-94: 剪贴板复制的空 catch 块应考虑用户反馈Line 76 和 Line 91 的空
catch块会静默吞掉错误。虽然复制失败不是关键功能,但建议至少在 fallback 失败时给用户一个提示。建议添加失败提示
if (clipboard) { void clipboard .writeText(sessionId) .then(() => toast.success(t("actions.copied"))) - .catch(() => {}); + .catch(() => toast.error(t("actions.copyFailed"))); return; } try { // ... fallback code ... if (ok) toast.success(t("actions.copied")); - } catch {} + } catch { + toast.error(t("actions.copyFailed")); + }注意:需要在 i18n 文件中添加
actions.copyFailed翻译 key。src/actions/usage-logs.ts (1)
307-322: 建议将 limit 提取为常量
limit: 20在两个分支中重复硬编码。建议提取为常量以便统一管理,与SESSION_ID_SUGGESTION_MIN_LEN等保持一致的风格。建议提取 limit 常量
const SESSION_ID_SUGGESTION_MIN_LEN = 2; const SESSION_ID_SUGGESTION_MAX_LEN = 128; +const SESSION_ID_SUGGESTION_DEFAULT_LIMIT = 20; // ... in getUsageLogSessionIdSuggestions ... const finalFilters = session.user.role === "admin" ? { term: trimmedTerm, userId: input.userId, keyId: input.keyId, providerId: input.providerId, - limit: 20, + limit: SESSION_ID_SUGGESTION_DEFAULT_LIMIT, } : { term: trimmedTerm, userId: session.user.id, keyId: input.keyId, providerId: input.providerId, - limit: 20, + limit: SESSION_ID_SUGGESTION_DEFAULT_LIMIT, };src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx (1)
95-135: 考虑简化 useMemo 依赖项。当前实现手动列举所有
searchParams字段并逐一传递给parseLogsUrlFilters,与usage-logs-view.tsx直接传递searchParams的方式不一致。如果后续新增筛选字段,需要同时修改多处。可以考虑使用
JSON.stringify(searchParams)作为稳定依赖键:♻️ 建议的简化方案
const filters = useMemo<VirtualizedLogsTableFilters & { page?: number }>(() => { - const parsed = parseLogsUrlFilters({ - userId: searchParams.userId, - keyId: searchParams.keyId, - providerId: searchParams.providerId, - sessionId: searchParams.sessionId, - startTime: searchParams.startTime, - endTime: searchParams.endTime, - statusCode: searchParams.statusCode, - model: searchParams.model, - endpoint: searchParams.endpoint, - minRetry: searchParams.minRetry, - page: searchParams.page, - }); + const parsed = parseLogsUrlFilters(searchParams); return { userId: parsed.userId, keyId: parsed.keyId, providerId: parsed.providerId, sessionId: parsed.sessionId, startTime: parsed.startTime, endTime: parsed.endTime, statusCode: parsed.statusCode, excludeStatusCode200: parsed.excludeStatusCode200, model: parsed.model, endpoint: parsed.endpoint, minRetryCount: parsed.minRetryCount, page: parsed.page, }; - }, [ - searchParams.userId, - searchParams.keyId, - searchParams.providerId, - searchParams.sessionId, - searchParams.startTime, - searchParams.endTime, - searchParams.statusCode, - searchParams.model, - searchParams.endpoint, - searchParams.minRetry, - searchParams.page, - ]); + }, [searchParams]);src/app/[locale]/dashboard/logs/_utils/time-range.ts (1)
7-19: 建议添加时间值范围校验。
parseClockString当前仅检查Number.isFinite,但未验证 hours (0-23)、minutes (0-59)、seconds (0-59) 的有效范围。超出范围的值可能导致new Date()产生意外的日期偏移。♻️ 建议的改进
export function parseClockString(clockStr: string): ClockParts { const [hoursRaw, minutesRaw, secondsRaw] = clockStr.split(":"); const hours = Number(hoursRaw); const minutes = Number(minutesRaw); const seconds = Number(secondsRaw ?? "0"); + const clampHours = Number.isFinite(hours) && hours >= 0 && hours <= 23 ? hours : 0; + const clampMinutes = Number.isFinite(minutes) && minutes >= 0 && minutes <= 59 ? minutes : 0; + const clampSeconds = Number.isFinite(seconds) && seconds >= 0 && seconds <= 59 ? seconds : 0; + return { - hours: Number.isFinite(hours) ? hours : 0, - minutes: Number.isFinite(minutes) ? minutes : 0, - seconds: Number.isFinite(seconds) ? seconds : 0, + hours: clampHours, + minutes: clampMinutes, + seconds: clampSeconds, }; }tests/unit/repository/usage-logs-sessionid-filter.test.ts (1)
54-76: 测试覆盖良好,但断言可更明确。当前测试断言
blankWhereSql === baseWhereSql来验证空白 sessionId 不会追加额外条件。建议额外断言 where 子句中不包含 "sessionid" 关键字,使测试意图更清晰:♻️ 建议的改进
expect(whereArgs).toHaveLength(2); const baseWhereSql = sqlToString(whereArgs[0]).toLowerCase(); const blankWhereSql = sqlToString(whereArgs[1]).toLowerCase(); expect(blankWhereSql).toBe(baseWhereSql); + expect(baseWhereSql).not.toContain("sessionid");src/app/[locale]/dashboard/logs/_utils/logs-query.ts (1)
69-70: 冗余 trim 操作。
sessionId在parseStringParam中已经被 trim 过(Line 50 调用),此处再次 trim 是冗余的。虽然不影响正确性,但可以移除以保持一致性。♻️ 建议的简化
- const sessionId = filters.sessionId?.trim(); - if (sessionId) query.set("sessionId", sessionId); + if (filters.sessionId) query.set("sessionId", filters.sessionId);src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (2)
197-208: requestKey 计算逻辑存在重复。
loadSessionIdsForFilter(Lines 201-207)和useEffect(Lines 249-255)中有相同的 requestKey 拼接逻辑。建议提取为独立的辅助函数或useMemo,避免重复代码并降低不一致风险。建议提取 requestKey 计算
const sessionIdRequestKey = useMemo(() => { const term = debouncedSessionIdSearchTerm.trim(); return [ term, isAdmin ? (localFilters.userId ?? "").toString() : "", (localFilters.keyId ?? "").toString(), (localFilters.providerId ?? "").toString(), isAdmin ? "1" : "0", ].join("|"); }, [debouncedSessionIdSearchTerm, isAdmin, localFilters.userId, localFilters.keyId, localFilters.providerId]);
718-722: 输入时立即 trim 可能导致用户体验问题。
onChange中对输入值调用.trim()会移除用户输入的前后空格,但如果用户在中间位置输入空格后继续输入,trim 不会影响。然而这里的问题是:当用户在输入框开头输入空格时,空格会被立即移除,可能导致光标位置异常或输入被"吞掉"的感觉。建议仅在提交/查询时进行 trim,而非每次 onChange 时:
onChange={(e) => { - const next = e.target.value.trim(); - setLocalFilters((prev) => ({ ...prev, sessionId: next || undefined })); - setSessionIdPopoverOpen(next.length >= SESSION_ID_SUGGESTION_MIN_LEN); + const next = e.target.value; + setLocalFilters((prev) => ({ ...prev, sessionId: next || undefined })); + setSessionIdPopoverOpen(next.trim().length >= SESSION_ID_SUGGESTION_MIN_LEN); }}实际过滤和后端查询时已有 trim 处理,前端输入框保持原值即可。
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (27)
.gitignoredocs/dashboard-logs-callchain.mdmessages/en/dashboard.jsonmessages/ja/dashboard.jsonmessages/ru/dashboard.jsonmessages/zh-CN/dashboard.jsonmessages/zh-TW/dashboard.jsonpackage.jsonsrc/actions/usage-logs.tssrc/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-table.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-view.tsxsrc/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsxsrc/app/[locale]/dashboard/logs/_utils/logs-query.tssrc/app/[locale]/dashboard/logs/_utils/time-range.tssrc/repository/usage-logs.tstests/unit/dashboard-logs-filters-time-range.test.tsxtests/unit/dashboard-logs-query-utils.test.tstests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsxtests/unit/dashboard-logs-time-range-utils.test.tstests/unit/repository/usage-logs-sessionid-filter.test.tstests/unit/repository/usage-logs-sessionid-suggestions.test.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxvitest.logs-sessionid-time-filter.config.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
tests/unit/dashboard-logs-time-range-utils.test.tssrc/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsxtests/unit/repository/usage-logs-sessionid-suggestions.test.tstests/unit/dashboard-logs-query-utils.test.tsvitest.logs-sessionid-time-filter.config.tssrc/actions/usage-logs.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsxsrc/app/[locale]/dashboard/logs/_utils/logs-query.tstests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsxsrc/repository/usage-logs.tstests/unit/dashboard-logs-filters-time-range.test.tsxtests/unit/repository/usage-logs-sessionid-filter.test.tssrc/app/[locale]/dashboard/logs/_components/usage-logs-table.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsxsrc/app/[locale]/dashboard/logs/_utils/time-range.tssrc/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
All new features must have unit test coverage of at least 80%
Files:
tests/unit/dashboard-logs-time-range-utils.test.tstests/unit/repository/usage-logs-sessionid-suggestions.test.tstests/unit/dashboard-logs-query-utils.test.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsxtests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsxtests/unit/dashboard-logs-filters-time-range.test.tsxtests/unit/repository/usage-logs-sessionid-filter.test.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
tests/unit/dashboard-logs-time-range-utils.test.tssrc/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsxtests/unit/repository/usage-logs-sessionid-suggestions.test.tstests/unit/dashboard-logs-query-utils.test.tsvitest.logs-sessionid-time-filter.config.tssrc/actions/usage-logs.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsxsrc/app/[locale]/dashboard/logs/_utils/logs-query.tstests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsxsrc/repository/usage-logs.tstests/unit/dashboard-logs-filters-time-range.test.tsxtests/unit/repository/usage-logs-sessionid-filter.test.tssrc/app/[locale]/dashboard/logs/_components/usage-logs-table.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsxsrc/app/[locale]/dashboard/logs/_utils/time-range.tssrc/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
tests/unit/dashboard-logs-time-range-utils.test.tssrc/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsxtests/unit/repository/usage-logs-sessionid-suggestions.test.tstests/unit/dashboard-logs-query-utils.test.tsvitest.logs-sessionid-time-filter.config.tssrc/actions/usage-logs.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsxsrc/app/[locale]/dashboard/logs/_utils/logs-query.tstests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsxsrc/repository/usage-logs.tstests/unit/dashboard-logs-filters-time-range.test.tsxtests/unit/repository/usage-logs-sessionid-filter.test.tssrc/app/[locale]/dashboard/logs/_components/usage-logs-table.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsxsrc/app/[locale]/dashboard/logs/_utils/time-range.tssrc/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx
tests/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Vitest for unit testing and happy-dom for DOM testing
Files:
tests/unit/dashboard-logs-time-range-utils.test.tstests/unit/repository/usage-logs-sessionid-suggestions.test.tstests/unit/dashboard-logs-query-utils.test.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxtests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsxtests/unit/dashboard-logs-filters-time-range.test.tsxtests/unit/repository/usage-logs-sessionid-filter.test.ts
src/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Source-adjacent tests should be placed in
src/**/*.test.tsalongside source files
Files:
src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx
src/repository/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM for data access in the repository layer
Files:
src/repository/usage-logs.ts
🧠 Learnings (9)
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : All new features must have unit test coverage of at least 80%
Applied to files:
tests/unit/dashboard-logs-time-range-utils.test.tstests/unit/dashboard-logs-query-utils.test.tsvitest.logs-sessionid-time-filter.config.tstests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsxtests/unit/dashboard-logs-filters-time-range.test.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to tests/**/*.test.{ts,tsx,js,jsx} : Use Vitest for unit testing and happy-dom for DOM testing
Applied to files:
tests/unit/dashboard-logs-time-range-utils.test.tspackage.jsontests/unit/dashboard-logs-query-utils.test.tsvitest.logs-sessionid-time-filter.config.tstests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsxtests/unit/dashboard-logs-filters-time-range.test.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer
Applied to files:
tests/unit/repository/usage-logs-sessionid-suggestions.test.ts
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).
Applied to files:
messages/zh-CN/dashboard.jsonmessages/en/dashboard.jsonmessages/ja/dashboard.json
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/actions/usage-logs.tssrc/app/[locale]/dashboard/logs/_utils/logs-query.tssrc/repository/usage-logs.tssrc/app/[locale]/dashboard/logs/_utils/time-range.ts
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.
Applied to files:
src/actions/usage-logs.ts
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Applied to files:
tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Applied to files:
tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.{ts,tsx} : The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext
Applied to files:
.gitignore
🧬 Code graph analysis (12)
tests/unit/dashboard-logs-time-range-utils.test.ts (1)
src/app/[locale]/dashboard/logs/_utils/time-range.ts (4)
parseClockString(7-19)dateStringWithClockToTimestamp(29-45)inclusiveEndTimestampFromExclusive(47-49)formatClockFromTimestamp(21-27)
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)
src/components/ui/tooltip.tsx (4)
TooltipProvider(57-57)Tooltip(57-57)TooltipTrigger(57-57)TooltipContent(57-57)
tests/unit/repository/usage-logs-sessionid-suggestions.test.ts (1)
src/repository/usage-logs.ts (1)
findUsageLogSessionIdSuggestions(570-617)
tests/unit/dashboard-logs-query-utils.test.ts (1)
src/app/[locale]/dashboard/logs/_utils/logs-query.ts (2)
parseLogsUrlFilters(34-60)buildLogsUrlQuery(62-93)
vitest.logs-sessionid-time-filter.config.ts (2)
scripts/sync-settings-keys.js (1)
path(15-15)scripts/validate-migrations.js (1)
__dirname(18-18)
src/actions/usage-logs.ts (4)
src/actions/types.ts (1)
ActionResult(31-31)src/lib/auth.ts (1)
getSession(116-128)src/repository/usage-logs.ts (1)
findUsageLogSessionIdSuggestions(570-617)src/lib/logger.ts (1)
logger(168-187)
src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx (1)
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx (1)
UsageLogsTable(45-567)
src/repository/usage-logs.ts (3)
src/drizzle/schema.ts (1)
messageRequest(276-362)src/repository/_shared/message-request-conditions.ts (1)
EXCLUDE_WARMUP_CONDITION(9-9)src/drizzle/db.ts (1)
db(37-44)
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx (3)
scripts/sync-settings-keys.js (1)
t(184-184)src/components/ui/table.tsx (2)
TableHead(92-92)TableCell(92-92)src/components/ui/tooltip.tsx (4)
TooltipProvider(57-57)Tooltip(57-57)TooltipTrigger(57-57)TooltipContent(57-57)
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (3)
src/lib/hooks/use-debounce.ts (1)
useDebounce(11-27)src/actions/usage-logs.ts (1)
getUsageLogSessionIdSuggestions(293-331)src/app/[locale]/dashboard/logs/_utils/time-range.ts (3)
formatClockFromTimestamp(21-27)inclusiveEndTimestampFromExclusive(47-49)dateStringWithClockToTimestamp(29-45)
src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx (2)
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)
VirtualizedLogsTableFilters(31-43)src/app/[locale]/dashboard/logs/_utils/logs-query.ts (2)
parseLogsUrlFilters(34-60)buildLogsUrlQuery(62-93)
src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx (1)
src/app/[locale]/dashboard/logs/_utils/logs-query.ts (2)
parseLogsUrlFilters(34-60)buildLogsUrlQuery(62-93)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: pr-review
- GitHub Check: Greptile Review
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: pr-description
- GitHub Check: pr-label
🔇 Additional comments (49)
.gitignore (1)
19-19: LGTM!新增的覆盖率目录忽略规则与现有模式一致,命名规范与
package.json中新增的测试脚本相对应。messages/ja/dashboard.json (1)
63-69: LGTM!日语翻译键值新增正确,命名和风格与现有翻译保持一致。sessionId 相关的 UI 字符串已按 i18n 要求添加到
logs.filters和logs.columns部分。Also applies to: 102-102
package.json (1)
21-21: LGTM!新增的测试覆盖率脚本遵循现有模式,与其他
test:coverage:*脚本保持一致的命名约定和配置结构。messages/zh-TW/dashboard.json (1)
63-69: LGTM!繁体中文翻译键值新增正确,保持 "Session ID" 等技术术语不翻译的风格与文件中其他技术术语(如 "API"、"Model"、"Token")一致。
Also applies to: 102-102
tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx (1)
69-69: LGTM!使用本地定义的
loadMessages()辅助函数替代之前的调用,简化了测试设置,同时保持了测试逻辑的完整性。messages/ru/dashboard.json (1)
63-69: LGTM!俄语本地化键值对添加正确,与英文版本保持一致,翻译符合上下文语义。
sessionId、searchSessionId、noSessionFound和columns.sessionId四个新键均已正确放置。Also applies to: 102-102
messages/en/dashboard.json (1)
63-69: LGTM!英文本地化键值对添加正确。
sessionId相关的四个新键(filters.sessionId、filters.searchSessionId、filters.noSessionFound、columns.sessionId)均已正确放置,文案清晰准确。Also applies to: 102-102
docs/dashboard-logs-callchain.md (2)
1-95: 文档结构清晰,边界定义明确。调用链盘点文档对本次需求的入口、链路、语义和影响面描述完整:
- 明确区分了虚拟列表与非虚拟表格两套链路
endExclusive = endInclusive + 1s的语义转换说明有助于理解前后端协作- "在范围内/不在范围内" 的边界定义有助于避免后续需求跑偏
67-67: 文件路径引用正确,无需修改。验证确认src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx在实际代码库中存在,与文档引用一致。src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx (1)
15-28: LGTM!
sessionId字段的添加方式正确:
- 作为可选字段加入
filters接口- 通过
JSON.stringify(filters)自动参与filtersKey计算,确保 sessionId 变化时缓存失效- 无需额外修改加载逻辑,现有
loadStats已正确依赖filters对象vitest.logs-sessionid-time-filter.config.ts (1)
11-61: 配置结构清晰,覆盖目标明确。专项覆盖率配置合理:
- 测试文件列表与覆盖目标文件(
logs-query.ts、time-range.ts)对应完整- 90% 阈值符合 PR 目标描述
setupFiles路径tests/setup.ts存在- mock 重置策略完整(
mockReset、restoreMocks、clearMocks)happy-dom环境与项目规范一致- 所有引用的测试文件均存在且位置正确
path模块导入正确,别名配置有效tests/unit/dashboard-logs-time-range-utils.test.ts (1)
1-46: 测试覆盖全面,结构清晰。测试用例覆盖了主要功能和边界情况:
parseClockString的 HH:MM 格式和无效输入处理dateStringWithClockToTimestamp的有效/无效日期处理inclusiveEndTimestampFromExclusive的往返验证和零值钳制formatClockFromTimestamp的格式化输出需要注意:Line 19-23 和 Line 42-45 的测试依赖本地时区,在不同时区的 CI 环境中可能产生不一致结果。如果遇到问题,可以考虑使用固定时区或 mock
Date。tests/unit/dashboard-logs-query-utils.test.ts (1)
1-90: 测试覆盖完整,验证了 URL 参数的解析与构建逻辑。测试用例覆盖了关键场景:
sessionId的 trim 处理- 数组参数取首值
statusCode: "!200"到excludeStatusCode200的映射- 无效数字的
undefined处理page的边界值验证build + parse往返一致性测试质量良好,符合 80% 覆盖率要求。
messages/zh-CN/dashboard.json (1)
63-69: i18n 翻译键添加正确。新增的翻译键与现有风格一致,技术术语 "Session ID" 保持英文是合理的做法。
Also applies to: 102-102
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (2)
214-219: Session ID 列实现正确,UI 一致性良好。新增的 Session ID 列:
- 表头使用 i18n 键
logs.columns.sessionId- 单元格提供 Tooltip 显示完整 ID
- 点击复制功能与整体交互风格一致
- 无数据时显示 muted dash 符合现有模式
Also applies to: 327-352
35-35:VirtualizedLogsTableFilters接口扩展正确。新增
sessionId?: string字段与LogsUrlFilters类型保持一致。tests/unit/repository/usage-logs-sessionid-suggestions.test.ts (3)
1-34: 辅助函数设计合理
sqlToString递归遍历 SQL 对象结构的实现能正确处理 Drizzle ORM 生成的嵌套 SQL 片段。使用visitedSet 防止循环引用是一个好的防御性编程实践。
36-68: Mock query builder 实现正确
createThenableQuery正确模拟了 Drizzle 的链式调用 API,并通过opts参数捕获调用参数以便断言。Promise.resolve(result)的方式使其可 await。
70-84: 空白 term 测试覆盖充分正确验证了空白字符串
" "会直接返回空数组且不触发数据库查询,符合 repository 层的早期返回逻辑。tests/unit/dashboard-logs-filters-time-range.test.tsx (2)
29-53: renderWithIntl 工具函数设计良好正确使用 React 19 的
createRootAPI,并提供了unmount方法用于测试清理。NextIntlClientProvider配置了timeZone="UTC"确保时间计算一致性。
97-127: 页面字段泄漏测试有效此测试用例验证了 Apply Filter 时
page字段被正确过滤掉,防止分页状态泄漏到 URL 参数中。这是对 PR 中提到的回归修复的有效覆盖。tests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsx (4)
11-13: i18n mock 返回 key 本身是合理的测试策略
useTranslations: () => (key: string) => key使得测试可以通过 i18n key 查找元素(如logs.filters.searchSessionId),避免依赖具体翻译文本。
51-111: Popover mock 实现充分支持测试场景自定义的
PopoverContext和相关组件正确模拟了 Radix UI Popover 的核心行为,包括open状态管理和asChild模式。这种轻量级 mock 避免了引入完整 Radix 依赖的复杂性。
141-205: 防抖和最小长度测试覆盖全面测试正确验证了:
- 输入长度 < 2 时不触发请求
- 防抖时间内(299ms)不触发请求
- 防抖时间到达(300ms)后触发单次请求
时间步进测试策略(分步推进 299ms + 1ms)能精确验证边界行为。
256-321: Provider scope 变更测试验证了关键回归修复此测试对应 PR 描述中的回归修复"sessionId 联想会在 scope 变化时刷新请求"。正确验证了:
- 初始请求在 focus 后发出
- Provider 变更后触发第二次请求
- 第二次请求携带正确的
providerId: 1src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx (2)
164-188: sessionId 单元格实现符合现有模式新增的 sessionId 列正确复用了文件中其他列的 Tooltip 模式,通过
data-session-id属性传递数据避免了闭包问题。truncate类配合 Tooltip 展示完整值是良好的 UX 设计。
105-105: 表头和 colSpan 更新正确新增
logs.columns.sessionId表头列,并正确将空数据行的colSpan从 10 更新为 11,保持表格结构一致性。Also applies to: 118-118
src/actions/usage-logs.ts (2)
22-23: 常量定义清晰
SESSION_ID_SUGGESTION_MIN_LEN和SESSION_ID_SUGGESTION_MAX_LEN提供了明确的输入边界,便于维护和调整。
286-331: sessionId 联想 API 实现正确实现要点:
- 权限控制:非 admin 用户强制使用
session.user.id,防止越权查询- 输入验证:
trim()+slice()处理边界,长度不足时早期返回空数组- 错误处理:统一的 try-catch 和日志记录
src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx (2)
54-55: LGTM! 筛选条件解析逻辑清晰。使用
parseLogsUrlFilters统一解析 URL 参数,并通过page ?? 1确保页码有默认值,逻辑正确且符合预期。
148-151: LGTM! 筛选条件变更处理正确。使用
buildLogsUrlQuery构建查询参数,自动排除page参数(因为newFilters类型为Omit<typeof filters, "page">),这确保了应用新筛选条件时会重置到第一页,修复了 PR 描述中提到的"Apply Filter 不再携带旧 page"问题。src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx (1)
148-151: LGTM! 与非虚拟化版本保持一致。
handleFilterChange使用buildLogsUrlQuery构建查询参数,逻辑与usage-logs-view.tsx保持一致。src/app/[locale]/dashboard/logs/_utils/time-range.ts (2)
21-27: 注意:使用本地时区格式化。
formatClockFromTimestamp使用getHours()、getMinutes()、getSeconds()基于本地时区格式化。请确认这与 UI 上显示的时间语义一致(用户期望看到本地时间)。如果需要 UTC 时间,应使用getUTCHours()等方法。
47-49: 函数命名与实际用途需确认。
inclusiveEndTimestampFromExclusive从命名看是将"排他上界"转换为"包含上界"(减 1 秒)。但 PR 描述提到"UI 的包含式 end 在发送后端前转换为排他上界 +1s",这是相反的转换方向。请确认此函数的使用场景:
- 如果用于显示(将后端的排他时间戳转为 UI 显示的包含时间),当前实现正确
- 如果用于提交(将 UI 包含时间转为后端排他时间),则应为
+1000而非-1000tests/unit/repository/usage-logs-sessionid-filter.test.ts (3)
3-34: LGTM!sqlToString辅助函数实现合理。使用
visitedSet 防止循环引用导致的无限递归,递归遍历 SQL 对象结构提取字符串值。这是合理的测试辅助实现。
78-98: LGTM! sessionId trim 精确匹配测试。正确验证了 sessionId 会被 trim 后再进行匹配,确保
"abc"出现在 where 子句中而" abc "不会出现。
100-162: 测试覆盖全面。为
findUsageLogsWithDetails和findUsageLogsStats提供了相同模式的测试覆盖,确保 sessionId 过滤逻辑在所有查询路径上保持一致。Also applies to: 164-206, 208-266, 268-308
src/app/[locale]/dashboard/logs/_utils/logs-query.ts (4)
1-14: LGTM! 接口定义清晰。
LogsUrlFilters接口涵盖所有筛选字段,使用?标记可选字段。字段类型与 URL 参数语义匹配。
16-32: LGTM! 辅助解析函数实现正确。
firstString正确处理 Next.js searchParams 的string | string[] | undefined类型parseIntParam使用Number.isFinite过滤 NaN 和 InfinityparseStringParam返回 trim 后的非空字符串或 undefined
34-60: LGTM! 解析逻辑完整。
parseLogsUrlFilters正确处理:
statusCode === "!200"的特殊情况设置excludeStatusCode200: true- 页码小于 1 时返回
undefined- 所有字段的类型转换和边界处理
88-90:page > 1时才写入 URL 的设计合理。这确保了默认第一页不会在 URL 中显示冗余参数,保持 URL 简洁。与
parseLogsUrlFilters中page ?? 1的默认值处理配合良好。src/repository/usage-logs.ts (4)
15-16: LGTM!
sessionId字段添加到UsageLogFilters接口,文档注释清晰说明了精确匹配语义和空值处理逻辑。
147-150: sessionId 过滤逻辑实现正确。trim + 空值检查的模式在
findUsageLogsBatch、findUsageLogsWithDetails、findUsageLogsStats三个函数中保持一致,确保了过滤行为的统一性。
605-608: 条件 JOIN 优化逻辑正确。仅在
keyId过滤器生效时才innerJoin keysTable,避免了无意义的 JOIN 操作,提升了查询性能。
570-617:findUsageLogSessionIdSuggestions实现良好。
- limit 使用
Math.min(50, Math.max(1, ...))进行边界约束- 空 term 提前返回空数组
- 按
min(created_at)降序排序,优先展示最近活跃的 sessionId- 结果过滤确保返回非空字符串
实现符合联想查询的预期行为。
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (4)
321-348: LGTM! handleApply 白名单模式有效防止 page 泄漏。显式解构并传递 filter 属性,而非直接传递整个
localFilters对象,确保了只有预期的过滤条件被提交,符合 PR 描述中修复的回归点。
398-414: 时间显示逻辑正确。使用
inclusiveEndTimestampFromExclusive将排他上界 (exclusive endTime) 转换为包含式时间戳用于 UI 显示,与formatClockFromTimestamp配合正确展示秒级精度。
463-512: 时间输入控件实现合理。
- 使用
type="time"配合step={1}支持秒级精度- 日期未选择时禁用时间输入 (
disabled={!displayStartDate})- 更新逻辑正确处理 inclusive/exclusive 转换
实现符合需求。
706-761: Session ID 联想 UI 实现完整。
- 使用
PopoverAnchor将 Input 作为锚点,避免额外的 trigger 元素onOpenAutoFocus={(e) => e.preventDefault()}防止 Popover 打开时抢夺焦点shouldFilter={false}正确配置,因为过滤由后端完成- 加载状态和空结果状态都有合适的提示文案
实现符合 UX 预期。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review Summary
This PR adds second-level time filtering and Session ID filtering/suggestions to the Dashboard Logs page. The implementation is well-structured with proper error handling, i18n support across all 5 languages, and comprehensive test coverage.
PR Size: L
- Lines changed: 2143 (2006 additions + 137 deletions)
- Files changed: 27
Split Suggestion for L-sized PR: Consider splitting future similar PRs into:
- Backend changes (repository + actions)
- Frontend filter UI changes
- Table column additions
- Test files
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean (ILIKE query uses parameterized input, no SQL injection risk)
- Error handling - Clean (proper try/catch with logging in actions, graceful fallbacks in UI)
- Type safety - Clean (proper TypeScript types, no
anyusage in new code) - Documentation accuracy - Clean (new callchain doc accurately reflects implementation)
- Test coverage - Adequate (comprehensive unit tests for new functionality, 95.52% coverage reported)
- Code clarity - Good
Validation Notes
Potential concerns investigated and validated:
-
Session ID ILIKE query performance: The PR description acknowledges this is not scalable for large datasets but mitigates with debounce (300ms), minLen (2 chars), and limit (20/50). This is documented as out-of-scope for optimization in this PR.
-
handleApply whitelist pattern: The explicit field whitelist in
handleApply()correctly preventspagefield leakage - this is the documented regression fix and is tested. -
Clipboard API error handling: The
handleCopySessionIdClickfunction properly catches clipboard errors and shows user feedback via toast. -
Time range edge cases: The
inclusiveEndTimestampFromExclusivefunction properly clamps at 0 to prevent negative timestamps. -
i18n completeness: All 5 required languages (en, ja, ru, zh-CN, zh-TW) have the new translation keys added.
Automated review by Claude AI
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
|
修复了上面 Gemini 以及 CodeRabbit 提出的问题与优化建议 |
|
为了能够实现在请求中大海捞针,这个部分还需要实现一个功能,即在向客户端报错时,在 message 内附带 CCH 内部的 Session ID。否则还是很难通过用户反馈定位到具体请求。 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/v1/_lib/codex/chat-completions-handler.ts (2)
67-78: 缺少 Session ID 附加此处 OpenAI 格式的
model缺失错误直接返回 Response,未通过attachSessionIdToErrorResponse包装。与其他错误路径(如 Lines 49-60、149-159)的处理方式不一致。🔧 建议修复
if (!openAIRequest.model) { - return new Response( + const response = new Response( JSON.stringify({ error: { message: "Invalid request: model is required", type: "invalid_request_error", code: "missing_required_fields", }, }), { status: 400, headers: { "Content-Type": "application/json" } } ); + return await attachSessionIdToErrorResponse(session.sessionId, response); }
125-139: 转换错误同样缺少 Session ID 附加请求格式转换失败时,返回的错误响应未附加 Session ID,与其他错误路径处理不一致。
🔧 建议修复
} catch (transformError) { logger.error("[ChatCompletions] Request transformation failed:", { context: transformError, }); - return new Response( + const response = new Response( JSON.stringify({ error: { message: "Failed to transform request format", type: "invalid_request_error", code: "transformation_error", }, }), { status: 400, headers: { "Content-Type": "application/json" } } ); + return await attachSessionIdToErrorResponse(session.sessionId, response); }




背景 / 目标
本 PR 覆盖
/dashboard/logs(Usage Logs)页面的两类增强,并根据 2026-01-15 的 Code Review 结论修复关键回归点:startTime/endTime仍为毫秒;后端仍使用半开区间created_at < endTime)。sessionId精确筛选:前端提供联想下拉(模糊候选),但最终查询条件是精确匹配(trim后非空才生效)。page泄漏、联想请求去重忽略 scope 变化。主要改动
后端(Repository / Actions)
sessionId精确筛选(trim 后非空才生效):batch/details/stats 三条链路保持一致。ILIKE '%term%'模糊匹配 +MIN(created_at)排序 + limit clamp;非 admin 强制 userId 边界,admin 可跟随userId/keyId/providerId收敛候选集。keyId过滤需要生效时才 join keysTable,避免无意义 join 与潜在候选集丢失。前端(Dashboard Logs)
+1s)。page泄漏:在handleApply输出前做字段白名单化,丢弃多余字段。term|userId|keyId|providerId|isAdmin),scope 变化会触发重新请求刷新列表。0被吞掉、page 参数脏值穿透)。文档 & 测试
docs/dashboard-logs-callchain.mdvitest.logs-sessionid-time-filter.config.tspackage.json增test:coverage:logs-sessionid-time-filter.gitignore忽略coverage-logs-sessionid-time-filter修复的回归点(Code Review 2026-01-15)
page(修根因:filters 运行时对象可能携带page,需白名单化输出)。userId/keyId/providerId/isAdmin)时会重新请求并刷新。兼容性 / 风险
startTime/endTime仍为毫秒时间戳;后端语义仍为created_at < endTime(半开区间)。sessionId筛选仍为精确匹配;联想只是候选来源。ILIKE '%term%',在大数据量下不可扩展;本 PR 仅通过 debounce/minLen/limit 与按需 join 减压,非根治。验证与测试
已运行(本地):
bunx vitest run --config vitest.logs-sessionid-time-filter.config.ts --coverage(All files 95.52%)bun run buildbun run test(142 files passed,910 tests passed).env.local加载环境变量):bun run test(142 files passed,910 tests passed)bun run typecheckbun run lintCI 建议:
回滚方案
本 PR 可整体 revert;若只需回滚“回归修复”部分,可按提交拆分回滚(commit 列表见分支
dev..HEAD)。参考
docs/dashboard-logs-callchain.mdGreptile Summary
This PR adds second-precision time filtering and Session ID exact filtering/suggestions to the Dashboard Logs page, while fixing two critical regressions discovered during code review.
Key Enhancements
1. Second-Precision Time Filters
startTime/endTimeremain millisecond timestamps)created_at < endTime)2. Session ID Filtering
LIKE 'term%' ESCAPE '\\')escapeLike()utility prevents SQL injection via LIKE wildcardsidx_message_request_session_id_prefixwithvarchar_pattern_opsfor efficient prefix queriesCritical Regression Fixes
1. Page Param Leak (commit 60bf8f2)
pagefield that propagated through Apply FilterhandleApply()to drop non-filter fieldstests/unit/dashboard-logs-filters-time-range.test.tsx:97-1272. Suggestion Scope Change (commit d011c74)
term, ignored scope changes (userId/keyId/providerId/isAdmin)term|userId|keyId|providerId|isAdmintriggers reload when scope changestests/unit/dashboard-logs-sessionid-suggestions-ui.test.tsx:256-321Implementation Quality
keysjoin whenkeyIdfilter not presentdocs/dashboard-logs-callchain.mdRisks & Scalability
ILIKE 'term%'which doesn't scale to massive datasets; mitigated by debounce, min length, limit, and prefix indexvarchar_pattern_opsrequires pattern to be anchored at start (no substring matching)Confidence Score: 5/5
Important Files Changed
Sequence Diagram
sequenceDiagram participant User as User Browser participant Filters as UsageLogsFilters participant Action as usage-logs.ts (Action) participant Repo as usage-logs.ts (Repo) participant DB as PostgreSQL Note over User,DB: SessionId Suggestion Flow User->>Filters: types "ab" in sessionId input Filters->>Filters: debounce 300ms Filters->>Filters: check min length (>=2) Filters->>Filters: build scope key (term|userId|keyId|providerId|isAdmin) Filters->>Action: getUsageLogSessionIdSuggestions({term, userId, keyId, providerId}) Action->>Action: trim & truncate term to max length Action->>Action: enforce userId for non-admin Action->>Repo: findUsageLogSessionIdSuggestions(filters) Repo->>Repo: escapeLike(term) → escape %, _, \ Repo->>Repo: build pattern: "escapedTerm%" Repo->>DB: SELECT DISTINCT session_id WHERE session_id LIKE 'ab%' ESCAPE '\\' Note over DB: Uses idx_message_request_session_id_prefix (varchar_pattern_ops) DB-->>Repo: matching session_ids (sorted by MIN(created_at) DESC) Repo-->>Action: sessionIds[] Action-->>Filters: sessionIds[] Filters->>Filters: update availableSessionIds state Filters->>User: display suggestions in dropdown Note over User,DB: Apply Filter with SessionId & Time Range User->>Filters: enters sessionId "abc", start "2026-01-01 10:30:15", end "2026-01-02 14:45:30" User->>Filters: clicks Apply Filter Filters->>Filters: handleApply() - whitelist fields (drops 'page' if leaked) Filters->>Filters: convert inclusive end time to exclusive (+1s) Filters->>Action: onChange({sessionId: "abc", startTime, endTime, ...}) Note over Action: URL updated via router.push Note over User,DB: Load Logs with Filters Action->>Repo: findUsageLogsBatch({sessionId, startTime, endTime, ...}) Repo->>Repo: trim sessionId → "abc" Repo->>Repo: build WHERE conditions Repo->>DB: SELECT * WHERE session_id = 'abc' AND created_at >= start AND created_at < end Note over DB: Uses idx_message_request_session_id for exact match DB-->>Repo: log records Repo-->>Action: logs with sessionId column Action-->>User: display virtualized table with sessionId (truncated + tooltip + copy)