fix(dashboard): unify timezone handling#1231
Conversation
📝 WalkthroughWalkthrough该PR为整个仪表盘系统添加了时区支持:Redis缓存键包含时区维度、统计查询按时区分桶、日期选择器按时区计算、组件props传递时区配置,确保用户看到符合其系统时区设定的统计数据、日期过滤和相对时间显示。 Changes时区感知仪表盘缓存和统计
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add 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 |
There was a problem hiding this comment.
Code Review
This pull request introduces comprehensive timezone support across the dashboard, logs, and statistics views, ensuring that date ranges, relative times, and database queries are resolved using the configured system timezone. Additionally, it updates Redis cache keys to include the timezone context and automatically invalidates these caches when the system timezone is updated. The review feedback suggests a minor optimization in the leaderboard's DateRangePicker to reuse the already-zoned baseDate variable instead of redundantly recalculating the timezone formatting.
| } | ||
| default: | ||
| return { startDate: "2020-01-01", endDate: formatDate(new Date()) }; | ||
| return { startDate: "2020-01-01", endDate: formatDateInSystemTimeZone(now, timeZone) }; |
There was a problem hiding this comment.
Since baseDate is already computed as the zoned time in the target timezone (toZonedTime(now, timeZone)), we can directly use formatDate(baseDate) instead of calling formatDateInSystemTimeZone(now, timeZone). This improves consistency with the other cases in the switch statement and avoids redundant timezone calculations.
| return { startDate: "2020-01-01", endDate: formatDateInSystemTimeZone(now, timeZone) }; | |
| return { startDate: "2020-01-01", endDate: formatDate(baseDate) }; |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/actions/system-config.ts (1)
159-169: ⚡ Quick win仅在时区实际发生变化时才失效全部仪表盘缓存。
当前条件只判断
validated.timezone !== undefined。若设置表单在每次保存时都携带timezone字段(部分更新表单通常会回传完整负载),那么即使时区未改变,每次保存都会清空全部 overview/statistics/leaderboard 缓存,导致随后大量请求回源数据库重算。建议改为比较保存前后的实际值。此处已有
before与updated,可直接对比:♻️ 建议改动
- if (validated.timezone !== undefined) { + if (validated.timezone !== undefined && before?.timezone !== updated.timezone) { await Promise.all([ invalidateAllOverviewCaches(), invalidateAllStatisticsCaches(), invalidateAllLeaderboardCaches(), ]).catch((error) => { logger.warn("[SystemSettings] Failed to invalidate timezone-sensitive dashboard caches", { error, }); }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/actions/system-config.ts` around lines 159 - 169, 当前逻辑只判断 validated.timezone !== undefined 导致每次保存都会失效缓存;请改为比较保存前后实际时区值(使用 before.timezone 和 updated.timezone)并且仅当二者不相等时才调用 invalidateAllOverviewCaches, invalidateAllStatisticsCaches, invalidateAllLeaderboardCaches,保留原有的 Promise.all(...).catch(...) 错误捕获与 logger.warn;确保对 undefined/null 情况也能正确判断变化(例如使用严格不等 !== 或显式比较字符串化后的值)。src/app/[locale]/my-usage/_components/statistics-summary-card.tsx (1)
39-44: 💤 Low value初始状态可能使用不正确的时区计算
useState的初始化函数在组件首次挂载时执行,此时serverTimeZone可能尚未从父组件加载完成(如上下文代码片段所示,serverTimeZone是异步获取的)。这意味着初始dateRange可能使用providerTimeZone(或 "UTC")计算,而非serverTimeZone。虽然 Lines 62-67 的 effect 会在
effectiveTimeZone变化时重置dateRange,但这依赖于autoDateRangeRef.current为true。当前逻辑是正确的,但建议添加注释说明这一行为,以避免后续维护时产生困惑。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`[locale]/my-usage/_components/statistics-summary-card.tsx around lines 39 - 44, The initial dateRange state (created via useState calling getDefaultDateRange(effectiveTimeZone)) can be computed before serverTimeZone is available, so document this behavior: add a clear comment next to the dateRange state initialization explaining that the first render may use providerTimeZone/UTC, and that the effect which watches effectiveTimeZone (the logic that compares effectiveTimeZone with previousTimeZoneRef and resets dateRange when autoDateRangeRef.current is true) will update the dateRange once the correct serverTimeZone arrives; reference getDefaultDateRange, dateRange/setDateRange, effectiveTimeZone, autoDateRangeRef and previousTimeZoneRef in the comment so maintainers understand why the lazy init is safe.tests/unit/repository/admin-user-insights-overview.test.ts (1)
71-74: ⚡ Quick win移除该测试中的无用
messageRequestmock 字段:getUserOverviewMetrics仅使用usageLedger(由LEDGER_BILLING_CONDITION引入的usageLedger.blockedBy/endpoint),未引用messageRequest.blockedBy/endpoint;当前用例断言也不涉及messageRequest,因此测试里这段messageRequestmock(lines 71-74)可删以避免误导。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/repository/admin-user-insights-overview.test.ts` around lines 71 - 74, Remove the unused messageRequest mock fields from the test: delete the messageRequest object (the blockedBy and endpoint properties) since getUserOverviewMetrics only reads usageLedger via LEDGER_BILLING_CONDITION (usageLedger.blockedBy / usageLedger.endpoint) and the assertions in this test do not reference messageRequest; update the test setup to rely solely on usageLedger/LEDGER_BILLING_CONDITION values and remove any references to messageRequest in the test fixture.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/actions/system-config.ts`:
- Around line 159-169: 当前逻辑只判断 validated.timezone !== undefined
导致每次保存都会失效缓存;请改为比较保存前后实际时区值(使用 before.timezone 和 updated.timezone)并且仅当二者不相等时才调用
invalidateAllOverviewCaches, invalidateAllStatisticsCaches,
invalidateAllLeaderboardCaches,保留原有的 Promise.all(...).catch(...) 错误捕获与
logger.warn;确保对 undefined/null 情况也能正确判断变化(例如使用严格不等 !== 或显式比较字符串化后的值)。
In `@src/app/`[locale]/my-usage/_components/statistics-summary-card.tsx:
- Around line 39-44: The initial dateRange state (created via useState calling
getDefaultDateRange(effectiveTimeZone)) can be computed before serverTimeZone is
available, so document this behavior: add a clear comment next to the dateRange
state initialization explaining that the first render may use
providerTimeZone/UTC, and that the effect which watches effectiveTimeZone (the
logic that compares effectiveTimeZone with previousTimeZoneRef and resets
dateRange when autoDateRangeRef.current is true) will update the dateRange once
the correct serverTimeZone arrives; reference getDefaultDateRange,
dateRange/setDateRange, effectiveTimeZone, autoDateRangeRef and
previousTimeZoneRef in the comment so maintainers understand why the lazy init
is safe.
In `@tests/unit/repository/admin-user-insights-overview.test.ts`:
- Around line 71-74: Remove the unused messageRequest mock fields from the test:
delete the messageRequest object (the blockedBy and endpoint properties) since
getUserOverviewMetrics only reads usageLedger via LEDGER_BILLING_CONDITION
(usageLedger.blockedBy / usageLedger.endpoint) and the assertions in this test
do not reference messageRequest; update the test setup to rely solely on
usageLedger/LEDGER_BILLING_CONDITION values and remove any references to
messageRequest in the test fixture.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c95f11f7-ddd3-4de3-9789-bc92681d0320
📒 Files selected for processing (32)
src/actions/statistics.tssrc/actions/system-config.tssrc/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsxsrc/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.tssrc/app/[locale]/dashboard/leaderboard/user/[userId]/_components/user-insights-view.tsxsrc/app/[locale]/dashboard/logs/_components/filters/active-filters-display.tsxsrc/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsxsrc/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsxsrc/app/[locale]/dashboard/logs/_utils/time-range.tssrc/app/[locale]/my-usage/_components/statistics-summary-card.tsxsrc/app/[locale]/my-usage/_components/usage-logs-section.tsxsrc/app/[locale]/my-usage/_components/usage-logs-table.tsxsrc/app/api/admin/system-config/route.tssrc/components/ui/relative-time.tsxsrc/lib/redis/index.tssrc/lib/redis/leaderboard-cache.tssrc/lib/redis/overview-cache.tssrc/lib/redis/statistics-cache.tssrc/repository/admin-user-insights.tssrc/repository/statistics.tssrc/types/dashboard-cache.tstests/unit/dashboard-logs-time-range-utils.test.tstests/unit/dashboard/dashboard-cache-keys.test.tstests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsxtests/unit/redis/leaderboard-cache.test.tstests/unit/redis/overview-cache.test.tstests/unit/redis/statistics-cache.test.tstests/unit/repository/admin-user-insights-overview.test.tstests/unit/repository/statistics-timezone-buckets.test.tstests/unit/user-insights-filters.test.ts
Summary
Resolve dashboard, logs, my-usage, and leaderboard date ranges against the configured system timezone. Include timezone in dashboard Redis cache keys and invalidate timezone-sensitive caches when timezone settings change. Normalize statistics bucket serialization to prevent UTC/local date drift.
Problem
Dashboard views (statistics, logs, leaderboard, my-usage) were using inconsistent timezone handling:
Dateparsing, causing UTC/local date drift and off-by-one day shifts in charts (e.g. April 28 data showing under April 27)Related Issues
Solution
resolveSystemTimezone()(DB config -> env TZ -> UTC fallback) anddate-fns-tzutilities (toZonedTime,formatInTimeZone,fromZonedTime) to compute dates in the correct timezonetz:<timezone>segment to prevent stale data when timezone changesinvalidateAllOverviewCaches,invalidateAllStatisticsCaches,invalidateAllLeaderboardCachesnormalizeBucketInstantreplacesnormalizeBucketDateto correctly parse bucket timestamps through the system timezone, preventing UTC/local driftserializeChartBucketDatereplaces resolution-dependent date formatting with consistent ISO serialization, ensuring stable date keys across all time rangesChanges
Core Changes
src/repository/statistics.ts(+52/-19) — ReplacenormalizeBucketDatewithnormalizeBucketInstantthat parses through system timezone; add timezone parameter to all DB query functionssrc/repository/admin-user-insights.ts(+36/-27) — AddbuildSystemTimezoneDateConditionshelper for all date range queries; useAT TIME ZONEin SQL conditionssrc/lib/redis/leaderboard-cache.ts(+23/-5) — Include timezone in all leaderboard cache keys; addinvalidateAllLeaderboardCachessrc/lib/redis/overview-cache.ts(+30/-6) — Include timezone in overview cache keys; addinvalidateAllOverviewCaches; handle legacy key cleanupsrc/lib/redis/statistics-cache.ts(+42/-14) — Include timezone in statistics cache keys; addinvalidateAllStatisticsCaches; handle legacy key cleanupsrc/actions/statistics.ts(+6/-10) — Replace resolution-dependent date formatting withserializeChartBucketDatefor stable chart bucketssrc/actions/system-config.ts(+17/-0) — Invalidate dashboard caches when timezone setting changessrc/app/api/admin/system-config/route.ts(+16/-0) — Same cache invalidation for REST API pathUI Changes
src/app/[locale]/dashboard/leaderboard/_components/date-range-picker.tsx(+17/-8) — UseformatInTimeZone/toZonedTimefor all date range computationsrc/app/[locale]/dashboard/leaderboard/user/[userId]/_components/filters/types.ts(+26/-18) — Add timezone parameter to time preset resolutionsrc/app/[locale]/dashboard/logs/_components/filters/active-filters-display.tsx(+10/-3) — Display active filter dates in system timezonesrc/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx(+6/-2) — Use timezone-aware "today" for calendar disabled/ navigationsrc/app/[locale]/dashboard/logs/_utils/time-range.ts(+13/-18) — SimplifygetQuickDateRangeto usetoZonedTimethen plaindate-fnsformattingsrc/app/[locale]/my-usage/_components/statistics-summary-card.tsx(+22/-6) — UseformatInTimeZonefor default date range; auto-reset range on timezone changesrc/components/ui/relative-time.tsx(+4/-1) — Accept optionaltimeZoneoverride propsrc/types/dashboard-cache.ts(+30/-6) — Addtimezonefield to cache key types; add timezone-scoped cache key buildersTest Changes
tests/unit/dashboard-logs-time-range-utils.test.ts(+10/-0) — Add test for early-morning UTC in a different timezonetests/unit/dashboard/dashboard-cache-keys.test.ts(+21/-11) — Update cache key tests for timezone-scoped keystests/unit/redis/leaderboard-cache.test.ts(+29/-1) — Test timezone-scoped Redis keys and cross-timezone behaviortests/unit/redis/overview-cache.test.ts(+35/-14) — Test timezone-scoped keys and invalidationtests/unit/redis/statistics-cache.test.ts(+70/-23) — Test timezone-scoped keys and DB parameter passingtests/unit/repository/admin-user-insights-overview.test.ts(+12/-0) — Add timezone-specific testtests/unit/repository/statistics-timezone-buckets.test.ts(+38/-0) — New file testing bucket normalization with timezonetests/unit/user-insights-filters.test.ts(+17/-0) — Test timezone-aware filter presetsBreaking Changes
None — all changes are backward-compatible:
tz:<timezone>, while legacy keys are still cleaned up on invalidationTesting
Automated Tests
bunx vitest runspecific tests passedbun run typecheckpassedbun run lintpassedbun run buildpassedManual Testing
Validation
bunx vitest run tests/unit/dashboard-logs-time-range-utils.test.ts tests/unit/dashboard/dashboard-cache-keys.test.ts tests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsx tests/unit/redis/leaderboard-cache.test.ts tests/unit/redis/overview-cache.test.ts tests/unit/redis/statistics-cache.test.ts tests/unit/repository/admin-user-insights-overview.test.ts tests/unit/repository/statistics-timezone-buckets.test.ts tests/unit/user-insights-filters.test.ts(passed)bun run typecheck(passed)bun run lint(passed; Biome schema version info only)bun run build(passed; existing Edge Runtime warnings in instrumentation dependencies)Description enhanced by Claude AI PR Agent