[codex] Add analytics overview filters#1496
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces analytics filtering, hourly metrics computation, and major dashboard UI modernization. The backend now captures user-agent headers from incoming event requests, supports dimensional filtering (country, referrer, browser, OS, device) on analytics queries via refactored ClickHouse joins, and computes hourly aggregates alongside daily metrics. The frontend gains animated slider indicators for pills and tabs, an interactive globe mode, standardized reduced-motion-aware chart animations, new top-list card components, and a fully restructured metrics page with filter controls, toggle-based chart mode selection, and period-based metric cards with sparkline animations. ChangesAnalytics Backend: Filtering, Hourly Metrics, and User-Agent Tracking
Dashboard UI: Animated Components, Filters, and Layout Refactoring
Shared Types, Exports, and Configuration
Sequence Diagram(s)sequenceDiagram
participant Handler as GET /metrics Handler
participant Normalize as normalizeAnalyticsOverviewFilters
participant LoadOverview as loadAnalyticsOverview
participant ClickHouse as ClickHouse
Handler->>Normalize: filters from query params
Normalize->>Handler: normalized filters object
Handler->>LoadOverview: analyticsFilters argument
LoadOverview->>LoadOverview: buildAnalyticsUserJoin with country
LoadOverview->>LoadOverview: construct dynamic SQL filter fragments
LoadOverview->>ClickHouse: execute parameterized daily/hourly/top-N queries
ClickHouse->>LoadOverview: aggregated results by dimension
LoadOverview->>Handler: parsed clickhouseAggregates with hourly/bounce/top-lists
Handler->>Handler: assemble response with new metric fields
sequenceDiagram
participant User
participant FilterMenu as Analytics Filter Menu
participant Preloader as MetricsFilterPreloader
participant LoadMetrics as useMetricsOrThrow
participant ChartWidget as Chart Widget
participant TopCards as Top Lists Cards
User->>FilterMenu: open filters dropdown
FilterMenu->>Preloader: trigger unfiltered preload
Preloader->>LoadMetrics: useMetricsOrThrow(adminApp, true, {})
LoadMetrics->>LoadMetrics: admin app useMetrics hook
Preloader->>ChartWidget: onReady callback
User->>FilterMenu: select country_code
FilterMenu->>Preloader: update filter state key
Preloader->>LoadMetrics: useMetricsOrThrow(adminApp, true, {country_code})
LoadMetrics->>LoadMetrics: admin cache with filters
ChartWidget->>ChartWidget: rebuild aggregates with selected filters
TopCards->>FilterMenu: onSelectCountry triggers filter toggle
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 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 docstrings
🧪 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 |
- Updated the globe control's minimum distance to use a dynamic camera distance. - Improved the layout of the globe section to prevent overflow. - Enhanced tooltip descriptions across various metrics for clarity and consistency, including adjustments to the "Unique Visitors" and "Avg. Session Time" tooltips. - Added tooltip support to the UserPageMetricCard component for better user experience.
- Added `framer-motion` version 12.39.0 to `pnpm-lock.yaml` and `package.json` for improved animation capabilities. - Integrated motion properties into various chart components in `line-chart.tsx` and `metrics-page.tsx` to enhance user experience with smoother transitions. - Refactored sparkline component in `user-page-metric-card.tsx` to support motion effects, improving visual feedback during data updates. - Introduced utility functions for handling reduced motion preferences to ensure accessibility compliance.
…ters - Removed the `classifyUserAgent` function from `route.tsx` to streamline user-agent handling. - Introduced `buildAnalyticsOverviewUserAgentFilterFragmentsForTest` to create user-agent filter fragments for analytics without relying on a raw user-agent allowlist. - Updated tests in `route.test.ts` to validate the new user-agent filter fragment generation. - Cleaned up unused code related to user-agent classification in the metrics module.
There was a problem hiding this comment.
Pull request overview
Adds richer analytics overview functionality across the internal metrics API and dashboard UI, including filterable breakdowns (country/referrer/browser/OS/device), 1‑day hourly series support, new overview cards, and UA capture with a server-side fallback for older clients.
Changes:
- Extend
/internal/metricsresponse + query params to support analytics overview filters, hourly series (1d), bounce/session breakdowns, and UA-derived top lists. - Update dashboard overview UI with filter chips/menu, new top-list cards, hourly chart rendering, and reduced-motion-aware animations.
- Capture
user_agenton analytics events client-side, plus a server-side header fallback.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds motion/framer-motion dependency lock entries. |
| apps/dashboard/package.json | Adds motion dependency for dashboard animations. |
| packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts | Sends user_agent on page-view events. |
| packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts | Adds filter-aware metrics caching + hook signature. |
| packages/stack-ui/src/components/ui/tooltip.tsx | Re-exports Radix TooltipPortal from stack-ui. |
| packages/stack-shared/src/interface/admin-metrics.ts | Extends metrics schemas/types for hourly series + new breakdown fields. |
| packages/stack-shared/src/interface/admin-interface.ts | Adds filter query params support to getMetrics(). |
| packages/dashboard-ui-components/src/components/tabs.tsx | Adds animated slider underline and reduced-motion handling. |
| packages/dashboard-ui-components/src/components/pill-toggle.tsx | Adds animated slider pill + icon-only tooltips and provider wrapping. |
| apps/dashboard/src/lib/stack-app-internals.ts | Adds AnalyticsOverviewFilters and threads filters into useMetricsOrThrow. |
| apps/dashboard/src/instrumentation.ts | Avoids Node-only import following into Edge instrumentation bundle. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/user-page-metric-card.tsx | Adds tooltip support + sparkline animation with reduced-motion support. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/overview/page.tsx | Adds redirect page for analytics overview route. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx | New reusable “top list” cards (referrers/regions/browsers/OS/devices). |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx | Major overview UI updates: filters, hourly 1d support, new cards/lists, animations. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx | Adds 1d range, hourly-friendly tick formatting, and chart motion props. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx | Adds optional interactive globe zoom behavior + layout tweaks. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe-section-with-data.tsx | Threads interactive flag through globe section wrapper. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.ts | New helper for toggling chart metric mode. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.test.ts | Adds unit tests for chart-mode toggling logic. |
| apps/backend/src/app/api/latest/internal/metrics/route.tsx | Adds filtering, hourly aggregates, bounce/session stats, UA breakdowns. |
| apps/backend/src/app/api/latest/internal/metrics/route.test.ts | Adds tests for filter normalization + UA filter fragment generation. |
| apps/backend/src/app/api/latest/analytics/events/batch/route.tsx | Adds server-side UA stamping fallback for older clients. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const latestHour = new Date(now); | ||
| latestHour.setMinutes(0, 0, 0); | ||
| for (let i = 23; i >= 0; i--) { | ||
| const hour = new Date(latestHour.getTime() - i * 60 * 60 * 1000); | ||
| const key = hour.toISOString().slice(0, 13); | ||
| const date = `${key}:00:00.000Z`; | ||
| hourlyPageViews.push({ date, activity: pageViewsByHour.get(key) ?? 0 }); |
| AND e.project_id = {projectId:String} | ||
| AND e.branch_id = {branchId:String} | ||
| AND e.event_at >= {hourlySince:DateTime} | ||
| AND e.event_at < {untilExclusive:DateTime} | ||
| AND ${analyticsContributingUserFilter} | ||
| ${sharedExtraFilters} | ||
| GROUP BY hour | ||
| ORDER BY hour ASC | ||
| `, | ||
| query_params: { | ||
| hourlySince: formatClickhouseDateTimeParam(new Date(now.getTime() - 23 * 60 * 60 * 1000)), | ||
| since: formatClickhouseDateTimeParam(since), | ||
| untilExclusive: formatClickhouseDateTimeParam(untilExclusive), | ||
| projectId: tenancy.project.id, |
| useMetrics: ( | ||
| includeAnonymous: boolean = false, | ||
| filters?: { country_code?: string, referrer?: string, browser?: string, os?: string, device?: string }, | ||
| ): MetricsResponse => { | ||
| const hasFilter = filters && Object.values(filters).some((v) => !!v); | ||
| const filtersKey = hasFilter ? JSON.stringify(filters) : ""; | ||
| return useAsyncCache(this._metricsCache, [includeAnonymous, filtersKey] as const, "adminApp.useMetrics()") as MetricsResponse; |
| useEffect(() => { | ||
| const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); | ||
| const updatePrefersReducedMotion = () => setPrefersReducedMotion(mediaQuery.matches); | ||
|
|
||
| updatePrefersReducedMotion(); | ||
| mediaQuery.addEventListener("change", updatePrefersReducedMotion); | ||
|
|
||
| return () => mediaQuery.removeEventListener("change", updatePrefersReducedMotion); |
| useEffect(() => { | ||
| const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); | ||
| const updatePrefersReducedMotion = () => setPrefersReducedMotion(mediaQuery.matches); | ||
|
|
||
| updatePrefersReducedMotion(); | ||
| mediaQuery.addEventListener("change", updatePrefersReducedMotion); | ||
|
|
||
| return () => mediaQuery.removeEventListener("change", updatePrefersReducedMotion); |
| } | ||
|
|
||
| function prefersReducedMotion(): boolean { | ||
| return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; |
| } | ||
|
|
||
| function prefersReducedMotion(): boolean { | ||
| return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; |
Greptile SummaryThis PR extends the analytics overview with dimension filters (country, referrer, browser, OS, device), 1-day hourly chart series, and new metric breakdowns (bounce rate, session duration, top browsers/OS/devices). It also captures
Confidence Score: 4/5Safe to merge; no data-loss or security issues found. The one inconsistency (local-time vs UTC hour truncation) only affects non-UTC server deployments, which are not typical for this stack. The analytics filter logic is correct and injection-safe throughout. The apps/backend/src/app/api/latest/internal/metrics/route.tsx — the hourly output loop at line 1688 uses Important Files Changed
Sequence DiagramsequenceDiagram
participant Browser
participant EventTracker
participant BatchRoute as /analytics/events/batch
participant MetricsRoute as /internal/metrics
participant ClickHouse
Browser->>EventTracker: page-view (with navigator.userAgent)
EventTracker->>BatchRoute: POST events[]
Note over BatchRoute: Stamp server UA if client UA missing
BatchRoute->>ClickHouse: INSERT events (user_agent in data blob)
Browser->>MetricsRoute: "GET ?filter_browser=Chrome&filter_country_code=US"
Note over MetricsRoute: normalizeAnalyticsOverviewFilters()<br/>builds named ClickHouse params
MetricsRoute->>ClickHouse: daily + hourly page-view queries (with filter fragments)
MetricsRoute->>ClickHouse: UA breakdown query (browser/OS/device cross-filter)
MetricsRoute->>ClickHouse: top referrers (country + UA filters only)
MetricsRoute->>ClickHouse: top regions (referrer + UA filters only)
MetricsRoute->>ClickHouse: session bounce/duration query
ClickHouse-->>MetricsRoute: aggregated rows
MetricsRoute-->>Browser: MetricsResponse (top_browsers, top_regions, bounce_rate, ...)
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/backend/src/app/api/latest/internal/metrics/route.tsx:1687-1688
`setMinutes(0, 0, 0)` clears **local-time** minutes/seconds/ms, not UTC. Every other hourly function in this file (`loadHourlyUsers`, `loadHourlyActiveUsers`) uses `setUTCMinutes(0, 0, 0)`. On a server whose timezone is not UTC the hour boundary would be off, causing all 24 `pageViewsByHour.get(key)` lookups to miss (returning `0`) because the ClickHouse result keys are UTC-aligned (`toStartOfHour`) while the loop keys would be misaligned by the local timezone offset.
```suggestion
const latestHour = new Date(now);
latestHour.setUTCMinutes(0, 0, 0);
```
### Issue 2 of 2
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx:17-23
**Duplicated animation utilities**
`easeOutCubic`, `prefersReducedMotion`, and the `requestAnimationFrame` animation loop pattern are copy-pasted from `metrics-page.tsx`. If the animation parameters or the reduced-motion check logic change, both files need to be updated in sync. Extracting these three to a shared module (e.g., `animation-utils.ts` next to these files) would eliminate the drift risk.
Reviews (1): Last reviewed commit: "Merge branch 'dev' into codex/analytics-..." | Re-trigger Greptile |
| const latestHour = new Date(now); | ||
| latestHour.setMinutes(0, 0, 0); |
There was a problem hiding this comment.
setMinutes(0, 0, 0) clears local-time minutes/seconds/ms, not UTC. Every other hourly function in this file (loadHourlyUsers, loadHourlyActiveUsers) uses setUTCMinutes(0, 0, 0). On a server whose timezone is not UTC the hour boundary would be off, causing all 24 pageViewsByHour.get(key) lookups to miss (returning 0) because the ClickHouse result keys are UTC-aligned (toStartOfHour) while the loop keys would be misaligned by the local timezone offset.
| const latestHour = new Date(now); | |
| latestHour.setMinutes(0, 0, 0); | |
| const latestHour = new Date(now); | |
| latestHour.setUTCMinutes(0, 0, 0); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/backend/src/app/api/latest/internal/metrics/route.tsx
Line: 1687-1688
Comment:
`setMinutes(0, 0, 0)` clears **local-time** minutes/seconds/ms, not UTC. Every other hourly function in this file (`loadHourlyUsers`, `loadHourlyActiveUsers`) uses `setUTCMinutes(0, 0, 0)`. On a server whose timezone is not UTC the hour boundary would be off, causing all 24 `pageViewsByHour.get(key)` lookups to miss (returning `0`) because the ClickHouse result keys are UTC-aligned (`toStartOfHour`) while the loop keys would be misaligned by the local timezone offset.
```suggestion
const latestHour = new Date(now);
latestHour.setUTCMinutes(0, 0, 0);
```
How can I resolve this? If you propose a fix, please make it concise.| } | ||
|
|
||
| function prefersReducedMotion(): boolean { | ||
| return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; | ||
| } | ||
|
|
||
| function useAnimatedBarValues(rows: Array<{ id: string, value: number }>): Map<string, number> { |
There was a problem hiding this comment.
Duplicated animation utilities
easeOutCubic, prefersReducedMotion, and the requestAnimationFrame animation loop pattern are copy-pasted from metrics-page.tsx. If the animation parameters or the reduced-motion check logic change, both files need to be updated in sync. Extracting these three to a shared module (e.g., animation-utils.ts next to these files) would eliminate the drift risk.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx
Line: 17-23
Comment:
**Duplicated animation utilities**
`easeOutCubic`, `prefersReducedMotion`, and the `requestAnimationFrame` animation loop pattern are copy-pasted from `metrics-page.tsx`. If the animation parameters or the reduced-motion check logic change, both files need to be updated in sync. Extracting these three to a shared module (e.g., `animation-utils.ts` next to these files) would eliminate the drift risk.
How can I resolve this? If you propose a fix, please make it concise.| import { cn, Spinner, Tooltip, TooltipContent, TooltipTrigger } from "@stackframe/stack-ui"; | ||
| import { TooltipPortal } from "@radix-ui/react-tooltip"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { cn, Spinner, Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from "@stackframe/stack-ui"; |
There was a problem hiding this comment.
| import { cn, Spinner, Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from "@stackframe/stack-ui"; | |
| import { TooltipPortal } from "@radix-ui/react-tooltip"; | |
| import { cn, Spinner, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@stackframe/stack-ui"; |
Incorrect import of TooltipPortal from @stackframe/stack-ui instead of @radix-ui/react-tooltip causes build failure
There was a problem hiding this comment.
7 issues found across 23 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx:118">
P2: This function will throw (via `parseChartDate`) for unsupported date formats instead of gracefully falling back to `value`. The `Number.isNaN` check is unreachable because `parseChartDate` never returns an invalid Date—it throws first. Wrap the call in a try/catch so the chart doesn't crash on unexpected input.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx:694">
P2: The non-interactive branch should also set `controls.enableZoom = false`. Without it, scroll events may still be captured by OrbitControls (preventing page scroll) even though min/max distance are equal.</violation>
</file>
<file name="packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts">
<violation number="1" location="packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts:589">
P2: Cache key includes empty-string filter values and is sensitive to property order, risking unnecessary cache misses. Consider stripping falsy values and sorting keys before serialization to produce a canonical key.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx:127">
P2: Potential hydration mismatch: `regionName` uses `navigator.language` at render time, which returns `"en"` on the server but the user's locale on the client. For non-English users, `Intl.DisplayNames` will produce different region names between SSR and hydration. Consider always using `"en"` or deferring locale-dependent rendering to a `useEffect`.</violation>
</file>
<file name="apps/backend/src/app/api/latest/analytics/events/batch/route.tsx">
<violation number="1" location="apps/backend/src/app/api/latest/analytics/events/batch/route.tsx:147">
P1: When `event.data` is `null`, `typeof null === "object"` is `true` in JavaScript, so `baseData` will be `null`. The subsequent `baseData.user_agent` access will throw a `TypeError`. Add a null check to the condition.</violation>
</file>
<file name="apps/backend/src/app/api/latest/internal/metrics/route.tsx">
<violation number="1" location="apps/backend/src/app/api/latest/internal/metrics/route.tsx:1428">
P2: Align `hourlySince` to the start of the current UTC hour before subtracting 23 hours; using `now - 23h` introduces a partial first bucket and undercounts the first point.</violation>
<violation number="2" location="apps/backend/src/app/api/latest/internal/metrics/route.tsx:1688">
P1: Use UTC hour truncation here so generated hourly keys stay aligned with the ClickHouse `toStartOfHour` output; local-time truncation can shift buckets on non-UTC servers.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
| })(); | ||
|
|
||
| const rows = body.events.map((event) => { | ||
| const baseData = (typeof event.data === "object" && !Array.isArray(event.data)) |
There was a problem hiding this comment.
P1: When event.data is null, typeof null === "object" is true in JavaScript, so baseData will be null. The subsequent baseData.user_agent access will throw a TypeError. Add a null check to the condition.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/app/api/latest/analytics/events/batch/route.tsx, line 147:
<comment>When `event.data` is `null`, `typeof null === "object"` is `true` in JavaScript, so `baseData` will be `null`. The subsequent `baseData.user_agent` access will throw a `TypeError`. Add a null check to the condition.</comment>
<file context>
@@ -134,18 +134,36 @@ export const POST = createSmartRouteHandler({
+ })();
+
+ const rows = body.events.map((event) => {
+ const baseData = (typeof event.data === "object" && !Array.isArray(event.data))
+ ? (event.data as Record<string, unknown>)
+ : {};
</file context>
| const baseData = (typeof event.data === "object" && !Array.isArray(event.data)) | |
| const baseData = (event.data != null && typeof event.data === "object" && !Array.isArray(event.data)) |
| const hourlyActiveUsers: DataPoints = []; | ||
| const hourlyVisitors: DataPoints = []; | ||
| const latestHour = new Date(now); | ||
| latestHour.setMinutes(0, 0, 0); |
There was a problem hiding this comment.
P1: Use UTC hour truncation here so generated hourly keys stay aligned with the ClickHouse toStartOfHour output; local-time truncation can shift buckets on non-UTC servers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/app/api/latest/internal/metrics/route.tsx, line 1688:
<comment>Use UTC hour truncation here so generated hourly keys stay aligned with the ClickHouse `toStartOfHour` output; local-time truncation can shift buckets on non-UTC servers.</comment>
<file context>
@@ -1275,39 +1671,129 @@ async function loadAnalyticsOverview(tenancy: Tenancy, now: Date, includeAnonymo
+ const hourlyActiveUsers: DataPoints = [];
+ const hourlyVisitors: DataPoints = [];
+ const latestHour = new Date(now);
+ latestHour.setMinutes(0, 0, 0);
+ for (let i = 23; i >= 0; i--) {
+ const hour = new Date(latestHour.getTime() - i * 60 * 60 * 1000);
</file context>
| latestHour.setMinutes(0, 0, 0); | |
| latestHour.setUTCMinutes(0, 0, 0); |
| function formatChartXAxisTick(value: string): string { | ||
| const date = parseChartDate(value); | ||
| if (Number.isNaN(date.getTime())) { | ||
| return value; | ||
| } | ||
| if (value.includes("T")) { | ||
| return date.toLocaleTimeString("en-US", { hour: "numeric" }); | ||
| } | ||
| return `${date.toLocaleDateString("en-US", { month: "short" })} ${date.getDate()}`; |
There was a problem hiding this comment.
P2: This function will throw (via parseChartDate) for unsupported date formats instead of gracefully falling back to value. The Number.isNaN check is unreachable because parseChartDate never returns an invalid Date—it throws first. Wrap the call in a try/catch so the chart doesn't crash on unexpected input.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx, line 118:
<comment>This function will throw (via `parseChartDate`) for unsupported date formats instead of gracefully falling back to `value`. The `Number.isNaN` check is unreachable because `parseChartDate` never returns an invalid Date—it throws first. Wrap the call in a try/catch so the chart doesn't crash on unexpected input.</comment>
<file context>
@@ -114,6 +115,17 @@ function parseChartDate(dateValue: string): Date {
return parsed;
}
+function formatChartXAxisTick(value: string): string {
+ const date = parseChartDate(value);
+ if (Number.isNaN(date.getTime())) {
</file context>
| function formatChartXAxisTick(value: string): string { | |
| const date = parseChartDate(value); | |
| if (Number.isNaN(date.getTime())) { | |
| return value; | |
| } | |
| if (value.includes("T")) { | |
| return date.toLocaleTimeString("en-US", { hour: "numeric" }); | |
| } | |
| return `${date.toLocaleDateString("en-US", { month: "short" })} ${date.getDate()}`; | |
| function formatChartXAxisTick(value: string): string { | |
| let date: Date; | |
| try { | |
| date = parseChartDate(value); | |
| } catch { | |
| return value; | |
| } | |
| if (value.includes("T")) { | |
| return date.toLocaleTimeString("en-US", { hour: "numeric" }); | |
| } | |
| return `${date.toLocaleDateString("en-US", { month: "short" })} ${date.getDate()}`; | |
| } |
| controls.enableZoom = true; | ||
| controls.minDistance = cameraDistance; | ||
| controls.maxDistance = 600; | ||
| } else { |
There was a problem hiding this comment.
P2: The non-interactive branch should also set controls.enableZoom = false. Without it, scroll events may still be captured by OrbitControls (preventing page scroll) even though min/max distance are equal.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx, line 694:
<comment>The non-interactive branch should also set `controls.enableZoom = false`. Without it, scroll events may still be captured by OrbitControls (preventing page scroll) even though min/max distance are equal.</comment>
<file context>
@@ -686,15 +687,21 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
+ controls.enableZoom = true;
+ controls.minDistance = cameraDistance;
+ controls.maxDistance = 600;
+ } else {
+ controls.maxDistance = cameraDistance;
+ controls.minDistance = cameraDistance;
</file context>
| } else { | |
| } else { | |
| controls.enableZoom = false; |
| filters?: { country_code?: string, referrer?: string, browser?: string, os?: string, device?: string }, | ||
| ): MetricsResponse => { | ||
| const hasFilter = filters && Object.values(filters).some((v) => !!v); | ||
| const filtersKey = hasFilter ? JSON.stringify(filters) : ""; |
There was a problem hiding this comment.
P2: Cache key includes empty-string filter values and is sensitive to property order, risking unnecessary cache misses. Consider stripping falsy values and sorting keys before serialization to produce a canonical key.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts, line 589:
<comment>Cache key includes empty-string filter values and is sensitive to property order, risking unnecessary cache misses. Consider stripping falsy values and sorting keys before serialization to produce a canonical key.</comment>
<file context>
@@ -578,8 +581,13 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
+ filters?: { country_code?: string, referrer?: string, browser?: string, os?: string, device?: string },
+ ): MetricsResponse => {
+ const hasFilter = filters && Object.values(filters).some((v) => !!v);
+ const filtersKey = hasFilter ? JSON.stringify(filters) : "";
+ return useAsyncCache(this._metricsCache, [includeAnonymous, filtersKey] as const, "adminApp.useMetrics()") as MetricsResponse;
},
</file context>
|
|
||
| export function regionName(code: string): string { | ||
| try { | ||
| const dn = new Intl.DisplayNames([typeof navigator !== "undefined" ? navigator.language : "en"], { type: "region" }); |
There was a problem hiding this comment.
P2: Potential hydration mismatch: regionName uses navigator.language at render time, which returns "en" on the server but the user's locale on the client. For non-English users, Intl.DisplayNames will produce different region names between SSR and hydration. Consider always using "en" or deferring locale-dependent rendering to a useEffect.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx, line 127:
<comment>Potential hydration mismatch: `regionName` uses `navigator.language` at render time, which returns `"en"` on the server but the user's locale on the client. For non-English users, `Intl.DisplayNames` will produce different region names between SSR and hydration. Consider always using `"en"` or deferring locale-dependent rendering to a `useEffect`.</comment>
<file context>
@@ -0,0 +1,435 @@
+
+export function regionName(code: string): string {
+ try {
+ const dn = new Intl.DisplayNames([typeof navigator !== "undefined" ? navigator.language : "en"], { type: "region" });
+ return dn.of(code.toUpperCase()) ?? code;
+ } catch {
</file context>
| ORDER BY hour ASC | ||
| `, | ||
| query_params: { | ||
| hourlySince: formatClickhouseDateTimeParam(new Date(now.getTime() - 23 * 60 * 60 * 1000)), |
There was a problem hiding this comment.
P2: Align hourlySince to the start of the current UTC hour before subtracting 23 hours; using now - 23h introduces a partial first bucket and undercounts the first point.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/app/api/latest/internal/metrics/route.tsx, line 1428:
<comment>Align `hourlySince` to the start of the current UTC hour before subtracting 23 hours; using `now - 23h` introduces a partial first bucket and undercounts the first point.</comment>
<file context>
@@ -1151,51 +1395,85 @@ async function loadAnalyticsOverview(tenancy: Tenancy, now: Date, includeAnonymo
+ ORDER BY hour ASC
+ `,
+ query_params: {
+ hourlySince: formatClickhouseDateTimeParam(new Date(now.getTime() - 23 * 60 * 60 * 1000)),
+ since: formatClickhouseDateTimeParam(since),
+ untilExclusive: formatClickhouseDateTimeParam(untilExclusive),
</file context>
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx (1)
112-113: ⚡ Quick winUse guideline-compliant URL construction for remote image sources.
These URLs are currently built with raw interpolation; please switch to
urlStringor explicit encoding for consistency with project standards.As per coding guidelines, "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency".Also applies to: 293-294
🤖 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 `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx around lines 112 - 113, The image source URLs for the flag images are built via raw string interpolation (e.g., the JSX props setting src and srcSet that use the variable lower); replace these with guideline-compliant URL construction by using the project's urlString helper or by wrapping the dynamic segment with encodeURIComponent(lower) so the final URLs are constructed safely (apply the same change for the second occurrence around the other src/srcSet at the referenced block). Locate the JSX attributes that set src and srcSet for the flag image in top-lists.tsx and update them to use urlString(...) or explicit encoding of the lower variable before concatenation.
🤖 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.
Inline comments:
In `@apps/backend/src/app/api/latest/analytics/events/batch/route.tsx`:
- Around line 137-149: The code currently uses unsafe `as` casts in
headerUserAgent and when coercing event.data; replace those casts with proper
type guards: implement headerUserAgent by checking if
fullReq.headers["user-agent"] is an array (take index 0 if so) or a string (use
it directly) and otherwise treat as null, and validate each event's data with a
plain-object guard (typeof event.data === "object" && event.data !== null &&
!Array.isArray(event.data)); if an event.data fails this check, short-circuit
the request and return StatusError.BadRequest (from StatusError) instead of
silently coercing to {} — update the rows generation (the variable rows and the
event handling logic) to assume a validated plain object after this guard.
In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx`:
- Around line 1428-1430: Compute an hour-truncated "now" in UTC before deriving
hourlySince so the 24 hourly buckets align to full UTC hours: take the existing
now value, create nowHour by zeroing UTC minutes/seconds/milliseconds (e.g.,
setUTCMinutes(0,0,0)), then set hourlySince using
formatClickhouseDateTimeParam(new Date(nowHour.getTime() - 23 * 60 * 60 *
1000)). Keep since and untilExclusive as-is unless they also need alignment;
change only the hourlySince calculation in route.tsx to use the truncated
nowHour.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx:
- Around line 372-393: Replace the truthy checks that drop empty-string filter
values by using explicit null/undefined checks: in analyticsFiltersKey(filters:
AnalyticsOverviewFilters) iterate FILTER_DIMENSIONS and call
params.set(dimension, filters[dimension]) when filters[dimension] != null (so
empty string is preserved), and change hasAnalyticsFilters(filters:
AnalyticsOverviewFilters) to return FILTER_DIMENSIONS.some(dimension =>
filters[dimension] != null) so an explicit empty-string value counts as a set
filter; keep getFilterDimensionLabel unchanged.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/overview/page.tsx:
- Line 9: The redirect call is interpolating projectId directly which can
produce malformed URLs; update the redirect invocation that builds the path (the
redirect(...) in page.tsx) to encode the route param, e.g. use
encodeURIComponent(projectId) or the repo's urlString helper when constructing
`/projects/{projectId}`, ensuring you reference the existing redirect(...) call
and the projectId variable so the final URL is safely encoded.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/user-page-metric-card.tsx:
- Around line 260-264: When tooltip is present, wrap the labelNode in a
focusable trigger (e.g., a <button type="button">) instead of plain text so
keyboard users can open the SimpleTooltip; update the conditional that renders
SimpleTooltip to pass the focusable element as its child and preserve existing
props (tooltip, inline, className="w-fit"), add accessible attributes on the
trigger such as type="button" and an appropriate aria-label or aria-describedby
that references the tooltip content, and style the button to match the label
visual (remove default button appearance) so behavior and layout remain
unchanged.
In `@apps/dashboard/src/lib/stack-app-internals.ts`:
- Line 63: Create a proper typed contract for the admin internals instead of
using "as" casts: define an interface (e.g., AdminInternals) that declares the
expected hook signatures (for example useMetrics(includeAnonymous: boolean,
filters: Filters): MetricsResponse, the hook that returns UserActivityResponse,
and the hook that returns MetricsUserCounts), then after the existing typeof/in
checks narrow or assert internals to that interface (or write a small type-guard
that checks the presence/types of
useMetrics/useUserActivity/useMetricsUserCounts) so calls like useMetrics(...)
and the other hooks infer the correct return types and you can remove the
trailing "as MetricsResponse"/"as UserActivityResponse"/"as MetricsUserCounts"
casts.
In `@packages/dashboard-ui-components/src/components/pill-toggle.tsx`:
- Around line 123-127: The effect creates a ResizeObserver without checking
availability which can crash in environments lacking the API; wrap the
creation/usage of ResizeObserver with a guard (e.g., if (typeof ResizeObserver
!== "undefined")) so you only call new ResizeObserver(updateSliderMetrics),
observe(toggle) and observe(selectedButton) when the constructor exists, and
ensure the cleanup handles the absent observer (no-op if not created). Update
the block that declares resizeObserver, calls resizeObserver.observe, and the
return cleanup to be conditional around the ResizeObserver presence while still
calling updateSliderMetrics fallback if needed.
In `@packages/dashboard-ui-components/src/components/tabs.tsx`:
- Around line 172-177: The useEffect currently constructs ResizeObserver
unconditionally which will throw in environments without it; before calling new
ResizeObserver(updateSliderMetrics) check availability (e.g., if (typeof
ResizeObserver === "undefined") return) and only create/observe when present,
referencing the same updateSliderMetrics, tabList and selectedButton variables,
and ensure the effect still returns a no-op cleanup when ResizeObserver is
unavailable or disconnects the observer when created.
In `@packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts`:
- Around line 571-576: The _refreshUsers override currently only refreshes two
specific metric cache keys (this._metricsCache.refresh([false, ""]) and
this._metricsCache.refresh([true, ""])) which leaves other filtered/variant
entries stale; update _refreshUsers to invalidate or refresh all variants of the
metrics caches instead of just those two keys by iterating over or using a
wildcard/invalidate method on this._metricsCache and
this._metricsUserCountsCache so every cached filtered key is cleared/refreshed
when _refreshUsers runs (locate the override _refreshUsers and the calls to
this._metricsCache.refresh and this._metricsUserCountsCache.refresh to apply the
change).
---
Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx:
- Around line 112-113: The image source URLs for the flag images are built via
raw string interpolation (e.g., the JSX props setting src and srcSet that use
the variable lower); replace these with guideline-compliant URL construction by
using the project's urlString helper or by wrapping the dynamic segment with
encodeURIComponent(lower) so the final URLs are constructed safely (apply the
same change for the second occurrence around the other src/srcSet at the
referenced block). Locate the JSX attributes that set src and srcSet for the
flag image in top-lists.tsx and update them to use urlString(...) or explicit
encoding of the lower variable before concatenation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 541203d6-fcf6-4691-b714-70609abd891a
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (22)
apps/backend/src/app/api/latest/analytics/events/batch/route.tsxapps/backend/src/app/api/latest/internal/metrics/route.test.tsapps/backend/src/app/api/latest/internal/metrics/route.tsxapps/dashboard/package.jsonapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.test.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.tsapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe-section-with-data.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/overview/page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/user-page-metric-card.tsxapps/dashboard/src/instrumentation.tsapps/dashboard/src/lib/stack-app-internals.tspackages/dashboard-ui-components/src/components/pill-toggle.tsxpackages/dashboard-ui-components/src/components/tabs.tsxpackages/stack-shared/src/interface/admin-interface.tspackages/stack-shared/src/interface/admin-metrics.tspackages/stack-ui/src/components/ui/tooltip.tsxpackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.tspackages/template/src/lib/stack-app/apps/implementations/event-tracker.ts
| // Server-side fallback: stamp the request's User-Agent into the event data | ||
| // blob when the client didn't already include one. This lets the analytics | ||
| // overview aggregate device/browser/OS breakdowns even for older clients. | ||
| const headerUserAgent = (() => { | ||
| const raw = fullReq.headers["user-agent"]; | ||
| if (Array.isArray(raw)) return raw[0] ?? null; | ||
| return (raw as string | undefined) ?? null; | ||
| })(); | ||
|
|
||
| const rows = body.events.map((event) => { | ||
| const baseData = (typeof event.data === "object" && !Array.isArray(event.data)) | ||
| ? (event.data as Record<string, unknown>) | ||
| : {}; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/backend/src/app/api/latest/analytics/events/batch/route.tsx"
# Show the relevant region with line numbers
sed -n '110,190p' "$FILE"
# Also show the surrounding validation/error handling in the handler for context
rg -n "throw new|StatusError|StackAssertionError|BadRequest|analytics event data|Invalid" "$FILE" || true
# Show imports to confirm which error types/utilities are available in this file
sed -n '1,120p' "$FILE"Repository: hexclave/stack-auth
Length of output: 8501
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find where createSmartRouteHandler is implemented
rg -n "function createSmartRouteHandler|createSmartRouteHandler\s*=" -S "apps/backend/src" || true
# If found, display relevant error-handling sections
# (limit output by focusing on common catch blocks)
FILE=$(rg -l "createSmartRouteHandler" apps/backend/src | head -n 20 | tail -n 1)
echo "Last matching file: $FILE"
if [ -f "$FILE" ]; then
rg -n "catch\s*\(|StatusError|StackAssertionError|BadRequest|500" "$FILE" || true
sed -n '1,200p' "$FILE" | cat
fi
# Also scan for this handler file references to StatusError vs unhandled errors
rg -n "StackAssertionError" apps/backend/src/app/api/latest/analytics/events/batch/route.tsx || trueRepository: hexclave/stack-auth
Length of output: 6250
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ROUTE="apps/backend/src/app/api/latest/analytics/events/batch/route.tsx"
SMART="apps/backend/src/route-handlers/smart-route-handler.tsx"
echo "=== route.tsx (analytics batch) lines 120-190 ==="
sed -n '120,190p' "$ROUTE"
echo
echo "=== smart-route-handler.tsx: show error handling region ==="
# Print around likely catch blocks / error serialization
rg -n "catch\s*\(|StatusError|StackAssertionError|HexclaveAssertionError|serialize|toResponse|response" "$SMART" | head -n 50
# Also dump a window around the first substantial match near line ~216
sed -n '180,320p' "$SMART"Repository: hexclave/stack-auth
Length of output: 10436
🏁 Script executed:
#!/bin/bash
set -euo pipefail
SMART="apps/backend/src/route-handlers/smart-route-handler.tsx"
echo "=== smart-route-handler.tsx: catchError + handleApiRequest region ==="
# show from around class InternalServerError through catchError and createResponse usage
sed -n '1,120p' "$SMART"
echo
echo "=== smart-route-handler.tsx: handleApiRequest/catch path ==="
# locate handleApiRequest implementation region if present in this file
rg -n "function handleApiRequest|handleApiRequest\s*\(" "$SMART"
# print around last match
MATCH_LINE=$(rg -n "handleApiRequest\s*\(" "$SMART" | head -n 1 | cut -d: -f1 || true)
if [ -n "${MATCH_LINE:-}" ]; then
START=$((MATCH_LINE-60))
END=$((MATCH_LINE+120))
sed -n "${START},${END}p" "$SMART"
fi
echo
echo "=== smart-route-handler.tsx: createResponse / errorToNiceString usage ==="
rg -n "createResponse|errorToNiceString|toResponse|InternalServerError|captureError|catchError" "$SMART"
sed -n '120,220p' "$SMART"Repository: hexclave/stack-auth
Length of output: 20725
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the remainder of the analytics batch route for any other assumptions about event.data
ROUTE="apps/backend/src/app/api/latest/analytics/events/batch/route.tsx"
echo "=== Full event mapping usages in ${ROUTE} ==="
rg -n "event\.data|user_agent|mergedData|baseData" "$ROUTE"
echo
echo "=== Event schema section (request validation) ==="
# print around where yupObject defines events/data
rg -n "events:|yupMixed|event_type|event_at_ms|data:" "$ROUTE"
sed -n '60,120p' "$ROUTE"
echo
echo "=== Search other analytics endpoints for similar event.data handling ==="
# locate other analytics event batch routes
rg -n "analytics/events/batch|analytics_internal\.events|event_type:|yupMixed\(\)\.defined\(\)" apps/backend/src/app/api/latest/analytics -S || true
# If there are other routes with similar map/normalization, inspect them
MATCHES=$(rg -l "analytics_internal\.events|yupMixed\(\)\.defined\(\)" apps/backend/src/app/api/latest/analytics -S || true)
echo
echo "Matched files:"
echo "$MATCHES" | sed 's/^/ - /'
if [ -n "${MATCHES:-}" ]; then
for f in $(echo "$MATCHES" | head -n 5); do
echo
echo "---- ${f} (context around normalization) ----"
rg -n "map\\(\\(event\\)|event\\.data|user_agent|mergedData|baseData|stripLoneSurrogates" "$f" || true
sed -n '1,220p' "$f" | head -n 60 || true
done
fiRepository: hexclave/stack-auth
Length of output: 7351
Remove as casts and fail fast on non-object event.data in analytics batch ingestion
headerUserAgentuses(raw as string | undefined), andevent.datais cast toRecord<string, unknown>viaasand then silently coerced to{}when it’s not an object. This hides malformed client payloads (the request schema usesyupMixed()fordata) and can skew analytics.- Replace both casts with type guards and return
StatusError.BadRequestwhenevent.dataisn’t a plain object.
Proposed fix
+function isRecord(value: unknown): value is Record<string, unknown> {
+ return value != null && typeof value === "object" && !Array.isArray(value);
+}
+
const headerUserAgent = (() => {
const raw = fullReq.headers["user-agent"];
if (Array.isArray(raw)) return raw[0] ?? null;
- return (raw as string | undefined) ?? null;
+ return typeof raw === "string" ? raw : null;
})();
const rows = body.events.map((event) => {
- const baseData = (typeof event.data === "object" && !Array.isArray(event.data))
- ? (event.data as Record<string, unknown>)
- : {};
+ if (!isRecord(event.data)) {
+ throw new StatusError(StatusError.BadRequest, "Invalid analytics event data payload");
+ }
+ const baseData = event.data;🤖 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 `@apps/backend/src/app/api/latest/analytics/events/batch/route.tsx` around
lines 137 - 149, The code currently uses unsafe `as` casts in headerUserAgent
and when coercing event.data; replace those casts with proper type guards:
implement headerUserAgent by checking if fullReq.headers["user-agent"] is an
array (take index 0 if so) or a string (use it directly) and otherwise treat as
null, and validate each event's data with a plain-object guard (typeof
event.data === "object" && event.data !== null && !Array.isArray(event.data));
if an event.data fails this check, short-circuit the request and return
StatusError.BadRequest (from StatusError) instead of silently coercing to {} —
update the rows generation (the variable rows and the event handling logic) to
assume a validated plain object after this guard.
| hourlySince: formatClickhouseDateTimeParam(new Date(now.getTime() - 23 * 60 * 60 * 1000)), | ||
| since: formatClickhouseDateTimeParam(since), | ||
| untilExclusive: formatClickhouseDateTimeParam(untilExclusive), |
There was a problem hiding this comment.
Align hourly query bounds to full UTC hours.
Line 1428 uses now - 23h without truncating to the hour, but chart buckets are hour-aligned later. This makes the first bucket a partial hour and undercounts it.
Proposed fix
- hourlySince: formatClickhouseDateTimeParam(new Date(now.getTime() - 23 * 60 * 60 * 1000)),
+ hourlySince: (() => {
+ const latestHour = new Date(now);
+ latestHour.setUTCMinutes(0, 0, 0);
+ return formatClickhouseDateTimeParam(new Date(latestHour.getTime() - 23 * 60 * 60 * 1000));
+ })(),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| hourlySince: formatClickhouseDateTimeParam(new Date(now.getTime() - 23 * 60 * 60 * 1000)), | |
| since: formatClickhouseDateTimeParam(since), | |
| untilExclusive: formatClickhouseDateTimeParam(untilExclusive), | |
| hourlySince: (() => { | |
| const latestHour = new Date(now); | |
| latestHour.setUTCMinutes(0, 0, 0); | |
| return formatClickhouseDateTimeParam(new Date(latestHour.getTime() - 23 * 60 * 60 * 1000)); | |
| })(), | |
| since: formatClickhouseDateTimeParam(since), | |
| untilExclusive: formatClickhouseDateTimeParam(untilExclusive), |
🤖 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 `@apps/backend/src/app/api/latest/internal/metrics/route.tsx` around lines 1428
- 1430, Compute an hour-truncated "now" in UTC before deriving hourlySince so
the 24 hourly buckets align to full UTC hours: take the existing now value,
create nowHour by zeroing UTC minutes/seconds/milliseconds (e.g.,
setUTCMinutes(0,0,0)), then set hourlySince using
formatClickhouseDateTimeParam(new Date(nowHour.getTime() - 23 * 60 * 60 *
1000)). Keep since and untilExclusive as-is unless they also need alignment;
change only the hourlySince calculation in route.tsx to use the truncated
nowHour.
| function analyticsFiltersKey(filters: AnalyticsOverviewFilters): string { | ||
| const params = new URLSearchParams(); | ||
| for (const dimension of FILTER_DIMENSIONS) { | ||
| const value = filters[dimension]; | ||
| if (value) { | ||
| params.set(dimension, value); | ||
| } | ||
| } | ||
| return params.toString(); | ||
| } | ||
|
|
||
| function getFilterDimensionLabel(dimension: keyof AnalyticsOverviewFilters): string { | ||
| const label = FILTER_DIMENSION_LABELS.get(dimension); | ||
| if (label == null) { | ||
| throw new Error(`Missing analytics filter dimension label: ${dimension}`); | ||
| } | ||
| return label; | ||
| } | ||
|
|
||
| function hasAnalyticsFilters(filters: AnalyticsOverviewFilters): boolean { | ||
| return FILTER_DIMENSIONS.some((dimension) => !!filters[dimension]); | ||
| } |
There was a problem hiding this comment.
Truthy checks in filter serialization can silently drop selected filter values.
analyticsFiltersKey/hasAnalyticsFilters use truthiness, so empty-string values are treated as “not set”. With referrer options allowing empty values (displayed as direct traffic), this can make selection appear to clear or not apply.
Suggested fix direction
function analyticsFiltersKey(filters: AnalyticsOverviewFilters): string {
const params = new URLSearchParams();
for (const dimension of FILTER_DIMENSIONS) {
const value = filters[dimension];
- if (value) {
+ if (value != null) {
params.set(dimension, value);
}
}
return params.toString();
}
function hasAnalyticsFilters(filters: AnalyticsOverviewFilters): boolean {
- return FILTER_DIMENSIONS.some((dimension) => !!filters[dimension]);
+ return FILTER_DIMENSIONS.some((dimension) => filters[dimension] != null);
}If empty referrer should represent “direct”, consider a non-empty sentinel value in UI state and explicit mapping before query construction.
As per coding guidelines, "Use explicit null/undefinedness checks (foo == null) instead of boolean checks (!foo) unless they are clearly equivalent from types".
Also applies to: 522-523
🤖 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
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
around lines 372 - 393, Replace the truthy checks that drop empty-string filter
values by using explicit null/undefined checks: in analyticsFiltersKey(filters:
AnalyticsOverviewFilters) iterate FILTER_DIMENSIONS and call
params.set(dimension, filters[dimension]) when filters[dimension] != null (so
empty string is preserved), and change hasAnalyticsFilters(filters:
AnalyticsOverviewFilters) to return FILTER_DIMENSIONS.some(dimension =>
filters[dimension] != null) so an explicit empty-string value counts as a set
filter; keep getFilterDimensionLabel unchanged.
|
|
||
| export default async function Page({ params }: { params: Promise<{ projectId: string }> }) { | ||
| const { projectId } = await params; | ||
| redirect(`/projects/${projectId}`); |
There was a problem hiding this comment.
Encode route param when building redirect URL.
Line 9 interpolates projectId directly into a path. Please encode it (or use `urlString``) to match the repo URL-construction rule and avoid malformed redirects for reserved characters.
Suggested fix
- redirect(`/projects/${projectId}`);
+ redirect(`/projects/${encodeURIComponent(projectId)}`);As per coding guidelines: "Use urlString`` or encodeURIComponent()` for URL construction instead of normal string interpolation, for consistency".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| redirect(`/projects/${projectId}`); | |
| redirect(`/projects/${encodeURIComponent(projectId)}`); |
🤖 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
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/overview/page.tsx
at line 9, The redirect call is interpolating projectId directly which can
produce malformed URLs; update the redirect invocation that builds the path (the
redirect(...) in page.tsx) to encode the route param, e.g. use
encodeURIComponent(projectId) or the repo's urlString helper when constructing
`/projects/{projectId}`, ensuring you reference the existing redirect(...) call
and the projectId variable so the final URL is safely encoded.
| {tooltip == null ? labelNode : ( | ||
| <SimpleTooltip tooltip={tooltip} inline className="w-fit"> | ||
| {labelNode} | ||
| </SimpleTooltip> | ||
| )} |
There was a problem hiding this comment.
Make the metric tooltip trigger keyboard reachable.
Line 260 wraps the label in SimpleTooltip, but the trigger is still just text, so keyboard users can't reach the new help copy. Use a focusable trigger (button or equivalent) here.
♿ Suggested fix
- {tooltip == null ? labelNode : (
- <SimpleTooltip tooltip={tooltip} inline className="w-fit">
- {labelNode}
- </SimpleTooltip>
- )}
+ {tooltip == null ? labelNode : (
+ <SimpleTooltip tooltip={tooltip} inline className="w-fit">
+ <button
+ type="button"
+ className="w-fit text-left"
+ aria-label={`${label}. ${tooltip}`}
+ >
+ {labelNode}
+ </button>
+ </SimpleTooltip>
+ )}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {tooltip == null ? labelNode : ( | |
| <SimpleTooltip tooltip={tooltip} inline className="w-fit"> | |
| {labelNode} | |
| </SimpleTooltip> | |
| )} | |
| {tooltip == null ? labelNode : ( | |
| <SimpleTooltip tooltip={tooltip} inline className="w-fit"> | |
| <button | |
| type="button" | |
| className="w-fit text-left" | |
| aria-label={`${label}. ${tooltip}`} | |
| > | |
| {labelNode} | |
| </button> | |
| </SimpleTooltip> | |
| )} |
🤖 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
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/user-page-metric-card.tsx
around lines 260 - 264, When tooltip is present, wrap the labelNode in a
focusable trigger (e.g., a <button type="button">) instead of plain text so
keyboard users can open the SimpleTooltip; update the conditional that renders
SimpleTooltip to pass the focusable element as its child and preserve existing
props (tooltip, inline, className="w-fit"), add accessible attributes on the
trigger such as type="button" and an appropriate aria-label or aria-describedby
that references the tooltip content, and style the button to match the label
visual (remove default button appearance) so behavior and layout remain
unchanged.
| } | ||
|
|
||
| return useMetrics(includeAnonymous) as MetricsResponse; | ||
| return useMetrics(includeAnonymous, filters) as MetricsResponse; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/dashboard/src/lib/stack-app-internals.ts"
echo "== File =="
wc -l "$FILE"
echo
echo "== Lines around 63 =="
nl -ba "$FILE" | sed -n '40,90p'
echo
echo "== Search for useMetrics / MetricsResponse definitions/usages =="
rg -n "useMetricsOrThrow|useMetrics\(|MetricsResponse|includeAnonymous|AnalyticsOverviewFilters" "$FILE" || true
echo
echo "== Search project for MetricsResponse type =="
rg -n "type\s+MetricsResponse|interface\s+MetricsResponse|MetricsResponse\s*=" apps/dashboard/src/lib "$FILE" || true
echo
echo "== Search for uses of 'as MetricsResponse' in repo =="
rg -n "as\s+MetricsResponse" . || trueRepository: hexclave/stack-auth
Length of output: 208
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/dashboard/src/lib/stack-app-internals.ts"
echo "== File =="
wc -l "$FILE"
echo
echo "== Lines around 63 =="
# show with line numbers
cat -n "$FILE" | sed -n '50,80p'
echo
echo "== Search for useMetrics / MetricsResponse definitions/usages =="
rg -n "useMetricsOrThrow|useMetrics\(|MetricsResponse|includeAnonymous|AnalyticsOverviewFilters" "$FILE" || true
echo
echo "== Find MetricsResponse definition in repo (scoped) =="
rg -n "type\s+MetricsResponse|interface\s+MetricsResponse|export\s+type\s+MetricsResponse|export\s+interface\s+MetricsResponse" apps/dashboard/src/lib || true
echo
echo "== Search for 'as MetricsResponse' in repo =="
rg -n "as\s+MetricsResponse" . || trueRepository: hexclave/stack-auth
Length of output: 2890
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/dashboard/src/lib/stack-app-internals.ts"
echo "== File header (lines 1-60) =="
cat -n "$FILE" | sed -n '1,60p'
echo
echo "== Locate stackAppInternalsSymbol definition/usages =="
rg -n "stackAppInternalsSymbol" . || true
echo
echo "== Inspect admin app internals typing (near other MetricsResponse casts) =="
sed -n '540,660p' packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts || true
echo
echo "== Inspect MetricsResponse import source =="
rg -n "export type MetricsResponse|type MetricsResponse" apps/dashboard/src/lib/stack-app-internals.ts packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts packages/stack-shared/src/interface/admin-interface.ts || trueRepository: hexclave/stack-auth
Length of output: 18513
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/dashboard/src/lib/stack-app-internals.ts"
echo "== Full file =="
cat "$FILE"
echo
echo "== Search for similar 'Reflect.get' internals type-guard patterns =="
rg -n "Reflect\.get\(.*stackAppInternalsSymbol|Admin app internals are unavailable|typeof internals !== \"object\"" apps/dashboard/src/lib || true
echo
echo "== Search for type-guard helpers like 'is .*Internals' in dashboard =="
rg -n "function .*is .*Internals|type .*Internals" apps/dashboard/src/lib || trueRepository: hexclave/stack-auth
Length of output: 6181
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/dashboard/src/lib/stack-app-internals.ts"
echo "== 'as *Response' in file =="
rg -n "as\s+(MetricsResponse|UserActivityResponse|MetricsUserCounts)\b" "$FILE" || trueRepository: hexclave/stack-auth
Length of output: 280
Replace as ...Response assertions by typing/narrowing the admin internals contract
apps/dashboard/src/lib/stack-app-internals.ts uses as MetricsResponse (line 63) and similar casts for other internals (as UserActivityResponse line 83, as MetricsUserCounts line 97). Instead of asserting the return type, narrow internals to a typed interface for the expected hook signatures (after the existing typeof ... === "function" / in checks) so the hook return type is inferred and the as assertions can be removed.
🤖 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 `@apps/dashboard/src/lib/stack-app-internals.ts` at line 63, Create a proper
typed contract for the admin internals instead of using "as" casts: define an
interface (e.g., AdminInternals) that declares the expected hook signatures (for
example useMetrics(includeAnonymous: boolean, filters: Filters):
MetricsResponse, the hook that returns UserActivityResponse, and the hook that
returns MetricsUserCounts), then after the existing typeof/in checks narrow or
assert internals to that interface (or write a small type-guard that checks the
presence/types of useMetrics/useUserActivity/useMetricsUserCounts) so calls like
useMetrics(...) and the other hooks infer the correct return types and you can
remove the trailing "as MetricsResponse"/"as UserActivityResponse"/"as
MetricsUserCounts" casts.
| const resizeObserver = new ResizeObserver(updateSliderMetrics); | ||
| resizeObserver.observe(toggle); | ||
| resizeObserver.observe(selectedButton); | ||
|
|
||
| return () => resizeObserver.disconnect(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether the codebase provides a ResizeObserver polyfill/test shim.
rg -n "ResizeObserver" --type ts --type tsx --type js --type jsx
rg -n "resize-observer-polyfill|`@juggle/resize-observer`|global\\.ResizeObserver|class ResizeObserver" --type ts --type tsx --type js --type jsxRepository: hexclave/stack-auth
Length of output: 119
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/dashboard-ui-components/src/components/pill-toggle.tsx"
echo "=== pill-toggle.tsx excerpt around ResizeObserver ==="
if [ -f "$FILE" ]; then
# Print a small window around the likely lines
nl -ba "$FILE" | sed -n '90,160p'
else
echo "Missing file: $FILE"
fi
echo
echo "=== Repo-wide ResizeObserver references (no invalid --type flags) ==="
rg -n --hidden --no-ignore-vcs "ResizeObserver" .
echo
echo "=== Polyfill/shim related references ==="
rg -n --hidden --no-ignore-vcs "resize[-_ ]observer|polyfill.*ResizeObserver|ResizeObserver.*polyfill|`@juggle/resize-observer`" .Repository: hexclave/stack-auth
Length of output: 178
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Locate pill-toggle.tsx ==="
fd -a "pill-toggle\.tsx" . || true
fd -a "pill-toggle\.ts" . || true
FILE="packages/dashboard-ui-components/src/components/pill-toggle.tsx"
echo
echo "=== Check file exists: $FILE ==="
if [ -f "$FILE" ]; then
echo "FOUND"
echo
echo "=== pill-toggle.tsx excerpt around ResizeObserver ==="
# Print a safe window; use cat -n since nl may not exist
cat -n "$FILE" | sed -n '100,150p'
else
echo "NOT FOUND"
fi
echo
echo "=== Repo-wide ResizeObserver references (limit to likely packages) ==="
rg -n --hidden --no-ignore-vcs "ResizeObserver" packages || true
echo
echo "=== Polyfill/shim related references ==="
rg -n --hidden --no-ignore-vcs "(resize[-_ ]observer|polyfill.*ResizeObserver|ResizeObserver.*polyfill|`@juggle/resize-observer`|global\.ResizeObserver|class ResizeObserver)" . || trueRepository: hexclave/stack-auth
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== pill-toggle.tsx: exact ResizeObserver usage (small window) ==="
sed -n '110,140p' packages/dashboard-ui-components/src/components/pill-toggle.tsx
echo
echo "=== Any guards around ResizeObserver in dashboard-ui-components ==="
rg -n --hidden --no-ignore-vcs "typeof ResizeObserver|ResizeObserver\\s*\\?|ResizeObserver\\s*===|if \\(?ResizeObserver" packages/dashboard-ui-components/src || true
echo
echo "=== Other components using ResizeObserver (quick context) ==="
rg -n --hidden --no-ignore-vcs "new ResizeObserver\\(" packages/dashboard-ui-components/src || true
echo
echo "=== Check how tests handle ResizeObserver ==="
rg -n --hidden --no-ignore-vcs "stubGlobal\\(\"ResizeObserver\"|MockResizeObserver|class MockResizeObserver" packages/dashboard-ui-components/src || true
echo
echo "=== Look for any polyfill/shim setup files (vitest/jest) ==="
rg -n --hidden --no-ignore-vcs "(setup.*test|vitest|jest|polyfill).*ResizeObserver|resize[-_ ]observer|`@juggle/resize-observer`|resize-observer-polyfill" packages . || trueRepository: hexclave/stack-auth
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for resize-observer polyfill deps/usages (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"(resize[-_ ]observer-polyfill|`@juggle/resize-observer`|polyfill.*ResizeObserver|ResizeObserver.*polyfill|global\\.ResizeObserver|stubGlobal\\(\"ResizeObserver\"|setup.*ResizeObserver)" .
echo
echo
echo "=== Search for test setup files (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"(setupFiles|setupFilesAfterEnv|setupTests|vitest\\.setup|testSetup|globalSetup|setup.*test)" .
echo
echo
echo "=== Search for ResizeObserver stubbing beyond data-grid.test.tsx (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"vi\\.stubGlobal\\(\"ResizeObserver\"|globalThis\\.ResizeObserver\\s*=|class MockResizeObserver" .
echo
echo
echo "=== Check whether pill-toggle has tests or is referenced in tests (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"pill-toggle|PillToggle" packages . || trueRepository: hexclave/stack-auth
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Find test runner config files likely to contain setup files (exclude node_modules) ==="
fd -a "vitest\.config\.(ts|js|mjs|cjs)" . --exec echo {} \; || true
fd -a "jest\.config\.(ts|js|mjs|cjs)" . --exec echo {} \; || true
fd -a "*vitest*.config.*" . --exec echo {} \; || true
fd -a "*jest*.config.*" . --exec echo {} \; || true
echo
echo "=== Search configs for setupFiles/setupTests and ResizeObserver (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"(setupFilesAfterEnv|setupFiles|setupTests|globalSetup|setup.*AfterEnv)" . \
--glob '!**/dist/**' --glob '!**/build/**' || true
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"ResizeObserver" \
"vitest.config.*" "jest.config.*" "vite.config.*" "package.json" . || true
echo
echo "=== Search for pill-toggle usage in tests (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"pill-toggle|PillToggle" packages/dashboard-ui-components --glob '*test*' || true
echo
echo "=== Search for local stubbing of ResizeObserver in non-data-grid tests (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"stubGlobal\\(\"ResizeObserver\"|MockResizeObserver|class MockResizeObserver" \
packages/dashboard-ui-components --glob '*test*' || trueRepository: hexclave/stack-auth
Length of output: 6552
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Setup files discovered earlier
SETUPS=(
"apps/e2e/tests/global-setup.ts"
"apps/backend/vitest.setup.ts"
)
echo "=== Inspect setup files for ResizeObserver ==="
for f in "${SETUPS[@]}"; do
if [ -f "$f" ]; then
echo
echo "--- $f (ResizeObserver references) ---"
rg -n "ResizeObserver" "$f" || true
echo
echo "--- $f (first 120 lines) ---"
sed -n '1,120p' "$f"
else
echo
echo "Missing file: $f"
fi
done
echo
echo "=== Find vitest config(s) relevant to packages/dashboard-ui-components ==="
fd -a "vitest\.config\.(ts|js|mjs|cjs)$" packages/dashboard-ui-components . || true
echo
echo "=== Search for dashboard-ui-components tests and whether ResizeObserver is stubbed there ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"ResizeObserver|stubGlobal\\(\"ResizeObserver\"|MockResizeObserver" \
packages/dashboard-ui-components/src --glob '*test*' || true
echo
echo "=== Search for any global test setup file that stubs ResizeObserver (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"(vitest\\.setup\\.ts|vitest\\.setup\\.js|setup.*\\.ts|global-setup\\.ts|setupFiles|setupFilesAfterEnv)" . || true
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"stubGlobal\\(\"ResizeObserver\"|globalThis\\.ResizeObserver\\s*=|MockResizeObserver" . || trueRepository: hexclave/stack-auth
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Locate apps/backend/src/polyfills ==="
fd -a "polyfills\.(ts|js|tsx)$" apps/backend . || true
echo
echo "=== Search for ResizeObserver in backend polyfills (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' "ResizeObserver" apps/backend/src || true
echo
echo "=== Search for any ResizeObserver polyfill packages in repo (exclude node_modules) ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' "`@juggle/resize-observer`|resize-observer-polyfill|`@react-hook/resize-observer`" package.json pnpm-lock.yaml yarn.lock package-lock.json . || trueRepository: hexclave/stack-auth
Length of output: 1692
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILES=(
"apps/backend/src/polyfills.tsx"
"apps/dashboard/src/polyfills.tsx"
)
for f in "${FILES[@]}"; do
echo "=== $f ==="
if [ -f "$f" ]; then
rg -n "ResizeObserver|resize[-_ ]observer" "$f" || true
echo "--- head (1-120) ---"
sed -n '1,120p' "$f"
echo "--- tail (last 80) ---"
tail -n 80 "$f"
else
echo "Missing: $f"
fi
echo
done
echo "=== Find vitest/jest config for dashboard-ui-components (exclude node_modules) ==="
fd -a "vitest\.config\.(ts|js|mjs|cjs)$|jest\.config\.(ts|js|mjs|cjs)$" packages/dashboard-ui-components . || true
echo
echo "=== Search for setupFiles usage in configs under dashboard-ui-components' workspace scope ==="
rg -n --hidden --no-ignore-vcs --glob '!**/node_modules/**' \
"setupFilesAfterEnv|setupFiles\\s*:\\s*\\[|setup.*AfterEnv|environment:\\s*['\"]jsdom['\"]" \
packages/dashboard-ui-components . || trueRepository: hexclave/stack-auth
Length of output: 10863
Guard ResizeObserver availability before creating it to avoid runtime/test crashes.
pill-toggle.tsx constructs new ResizeObserver(...) without checking if ResizeObserver exists; environments without this Web API will throw when the effect runs. No repo-wide polyfill/setup was found (only local stubs in data-grid tests).
Suggested fix
- const resizeObserver = new ResizeObserver(updateSliderMetrics);
- resizeObserver.observe(toggle);
- resizeObserver.observe(selectedButton);
-
- return () => resizeObserver.disconnect();
+ if (typeof ResizeObserver !== "function") {
+ return;
+ }
+ const resizeObserver = new ResizeObserver(updateSliderMetrics);
+ resizeObserver.observe(toggle);
+ resizeObserver.observe(selectedButton);
+
+ return () => resizeObserver.disconnect();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const resizeObserver = new ResizeObserver(updateSliderMetrics); | |
| resizeObserver.observe(toggle); | |
| resizeObserver.observe(selectedButton); | |
| return () => resizeObserver.disconnect(); | |
| if (typeof ResizeObserver !== "function") { | |
| return; | |
| } | |
| const resizeObserver = new ResizeObserver(updateSliderMetrics); | |
| resizeObserver.observe(toggle); | |
| resizeObserver.observe(selectedButton); | |
| return () => resizeObserver.disconnect(); |
🤖 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 `@packages/dashboard-ui-components/src/components/pill-toggle.tsx` around lines
123 - 127, The effect creates a ResizeObserver without checking availability
which can crash in environments lacking the API; wrap the creation/usage of
ResizeObserver with a guard (e.g., if (typeof ResizeObserver !== "undefined"))
so you only call new ResizeObserver(updateSliderMetrics), observe(toggle) and
observe(selectedButton) when the constructor exists, and ensure the cleanup
handles the absent observer (no-op if not created). Update the block that
declares resizeObserver, calls resizeObserver.observe, and the return cleanup to
be conditional around the ResizeObserver presence while still calling
updateSliderMetrics fallback if needed.
| const resizeObserver = new ResizeObserver(updateSliderMetrics); | ||
| resizeObserver.observe(tabList); | ||
| resizeObserver.observe(selectedButton); | ||
|
|
||
| return () => resizeObserver.disconnect(); | ||
| }, [categories, selectedCategory]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify all unguarded ResizeObserver usages in UI components.
rg -n "new ResizeObserver\\(" --type ts --type tsxRepository: hexclave/stack-auth
Length of output: 91
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find all ResizeObserver constructors in ts/tsx =="
rg -n "new ResizeObserver\\(" -g '*.ts' -g '*.tsx' . || true
echo
echo "== Inspect tabs.tsx around the reported lines =="
FILE="packages/dashboard-ui-components/src/components/tabs.tsx"
if [ -f "$FILE" ]; then
# print ~40 lines around the location
sed -n '140,210p' "$FILE" | nl -ba | sed 's/^/| /'
else
echo "Missing: $FILE" >&2
exit 1
fi
echo
echo "== Look for existing ResizeObserver guards/patterns in the codebase =="
rg -n "(typeof ResizeObserver|ResizeObserver\\s*in\\s*|window\\.ResizeObserver|globalThis\\.ResizeObserver)" -g '*.ts' -g '*.tsx' . || trueRepository: hexclave/stack-auth
Length of output: 2235
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/dashboard-ui-components/src/components/tabs.tsx"
echo "== tabs.tsx lines 160-190 =="
if [ -f "$FILE" ]; then
# add line numbers without `nl`
awk 'NR>=160 && NR<=190 { printf "%6d | %s\n", NR, $0 }' "$FILE"
else
echo "Missing: $FILE" >&2
exit 1
fi
echo
echo "== Search for existing ResizeObserver guards/patterns =="
rg -n "(typeof ResizeObserver|ResizeObserver\\s*in\\s*|window\\.ResizeObserver|globalThis\\.ResizeObserver)" -g '*.ts' -g '*.tsx' packages apps docs . || true
echo
echo "== Search for optional chaining / conditional usage patterns around ResizeObserver =="
rg -n "(if\\s*\\(.*ResizeObserver|ResizeObserver\\s*\\?\\.|\\?.*ResizeObserver|globalThis\\.ResizeObserver|window\\.ResizeObserver)" -g '*.ts' -g '*.tsx' . || trueRepository: hexclave/stack-auth
Length of output: 1284
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== pill-toggle.tsx around ResizeObserver =="
FILE="packages/dashboard-ui-components/src/components/pill-toggle.tsx"
if [ -f "$FILE" ]; then
awk 'NR>=90 && NR<=160 { printf "%6d | %s\n", NR, $0 }' "$FILE"
else
echo "Missing: $FILE" >&2; exit 1
fi
echo
echo "== data-grid.tsx around ResizeObserver =="
FILE="packages/dashboard-ui-components/src/components/data-grid/data-grid.tsx"
if [ -f "$FILE" ]; then
# print around both hits
awk 'NR>=780 && NR<=840 { printf "%6d | %s\n", NR, $0 }' "$FILE"
echo "----"
awk 'NR>=920 && NR<=990 { printf "%6d | %s\n", NR, $0 }' "$FILE"
else
echo "Missing: $FILE" >&2; exit 1
fi
echo
echo "== Search for explicit ResizeObserver guards across repo =="
rg -n "typeof\\s+ResizeObserver\\s*===\\s*'undefined'|typeof\\s+ResizeObserver\\s*!==\\s*'undefined'|ResizeObserver\\s*\\?\\.|window\\.ResizeObserver\\b|globalThis\\.ResizeObserver\\b" -g '*.ts' -g '*.tsx' . || trueRepository: hexclave/stack-auth
Length of output: 9538
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for ResizeObserver polyfills/shims =="
rg -n "resize-observer-polyfill|ResizeObserver\\s*polyfill|polyfill.*ResizeObserver" -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' -S . || true
echo
echo "== Search for guard utility/helper names =="
rg -n "use.*ResizeObserver|ResizeObserver.*(guard|fallback|available|supported|shim)" -S -g '*.ts' -g '*.tsx' . || true
echo
echo "== Check package.json for environment/polyfill hints =="
# Print only dependencies containing resizeobserver
for f in package.json packages/*/package.json apps/*/package.json; do
if [ -f "$f" ]; then
echo "--- $f ---"
rg -n "resize|observer" "$f" || true
fi
doneRepository: hexclave/stack-auth
Length of output: 2274
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/dashboard-ui-components/src/components/tabs.tsx"
echo "== tabs.tsx header (top ~80 lines) =="
if [ -f "$FILE" ]; then
sed -n '1,90p' "$FILE" | awk '{printf "%4d | %s\n", NR, $0}'
else
echo "Missing: $FILE" >&2; exit 1
fi
echo
echo "== Search for ResizeObserver polyfill/mock in repo =="
rg -n "(ResizeObserver).*\\b(mock|polyfill|shim|undefined|not\\s+defined|class\\s+ResizeObserver|jest\\.|jsdom)" -S -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' . || true
echo
echo "== Search common test setup files =="
rg -n "(setupTests|jest\\.setup|vitest\\.?setup|testSetup|jsdom)" -S -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' . || trueRepository: hexclave/stack-auth
Length of output: 4367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate vitest setup files
echo "== Find vitest.setup.* in repo =="
fd -t f "vitest.setup.*" . || true
fd -t f "*setup*.ts" apps/backend -x echo 2>/dev/null || true
echo
echo "== Inspect apps/backend/vitest.setup.ts =="
FILE="apps/backend/vitest.setup.ts"
if [ -f "$FILE" ]; then
sed -n '1,200p' "$FILE" | awk '{print NR"|"$0}'
else
echo "Missing $FILE" >&2
fi
echo
echo "== Search for ResizeObserver mocks in setup/config =="
rg -n "ResizeObserver\\b" -S -g '*.ts' -g '*.tsx' . || true
echo
echo "== Search for vitest setup files that mention ResizeObserver specifically =="
rg -n "ResizeObserver\\b" -S -g 'vitest*.ts' -g '*setup*.ts' . || trueRepository: hexclave/stack-auth
Length of output: 4334
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Inspect apps/backend/src/polyfills =="
FILE="apps/backend/src/polyfills.ts"
if [ -f "$FILE" ]; then
sed -n '1,200p' "$FILE" | awk '{print NR"|"$0}'
else
FILE2="apps/backend/src/polyfills/index.ts"
if [ -f "$FILE2" ]; then
sed -n '1,200p' "$FILE2" | awk '{print NR"|"$0}'
else
echo "Missing polyfills entrypoint in apps/backend/src/ (looked for polyfills.ts and polyfills/index.ts)" >&2
exit 1
fi
fi
echo
echo "== Re-check runtime guard patterns across UI components (ResizeObserver in conditionals) =="
rg -n "ResizeObserver\\b" -g '*.ts' -g '*.tsx' packages apps docs -S
echo
echo "== Search for patterns that guard browser APIs (window/document/globalThis checks) =="
rg -n "(globalThis|window)\\.(ResizeObserver|matchMedia)|typeof\\s+ResizeObserver|ResizeObserver\\s+in\\s+\\w+|if\\s*\\(.*ResizeObserver" -g '*.ts' -g '*.tsx' . || trueRepository: hexclave/stack-auth
Length of output: 221
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== List anything matching apps/backend/src/polyfills* =="
fd -t f -t d "polyfills" apps/backend/src || true
fd -t f -t d "polyfills" apps/backend || true
echo
echo "== Show apps/backend/vitest.setup.ts =="
sed -n '1,60p' apps/backend/vitest.setup.ts | awk '{print NR"|"$0}'
echo
echo "== Locate actual ResizeObserver handling outside tests =="
# Search for a browser guard helper usage patterns (generic)
rg -n "(ResizeObserver\\b)" -g '*.ts' -g '*.tsx' packages apps docs || trueRepository: hexclave/stack-auth
Length of output: 4253
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/backend/src/polyfills.tsx"
if [ -f "$FILE" ]; then
echo "== polyfills.tsx =="
sed -n '1,200p' "$FILE" | awk '{print NR"|"$0}'
else
echo "Missing $FILE" >&2
exit 1
fi
echo
echo "== Does polyfills.tsx mention ResizeObserver? =="
rg -n "ResizeObserver" "$FILE" || trueRepository: hexclave/stack-auth
Length of output: 2871
Add a ResizeObserver availability guard in tabs.tsx
ResizeObserver is constructed unconditionally in this useEffect; if the browser doesn’t support it, the effect will throw. Add an availability check before new ResizeObserver(...) (e.g. if (typeof ResizeObserver === "undefined") return;).
const resizeObserver = new ResizeObserver(updateSliderMetrics);
resizeObserver.observe(tabList);
resizeObserver.observe(selectedButton);
return () => resizeObserver.disconnect();🤖 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 `@packages/dashboard-ui-components/src/components/tabs.tsx` around lines 172 -
177, The useEffect currently constructs ResizeObserver unconditionally which
will throw in environments without it; before calling new
ResizeObserver(updateSliderMetrics) check availability (e.g., if (typeof
ResizeObserver === "undefined") return) and only create/observe when present,
referencing the same updateSliderMetrics, tabList and selectedButton variables,
and ensure the effect still returns a no-op cleanup when ResizeObserver is
unavailable or disconnects the observer when created.
| protected override async _refreshUsers() { | ||
| await Promise.all([ | ||
| super._refreshUsers(), | ||
| this._metricsCache.refresh([false]), | ||
| this._metricsCache.refresh([true]), | ||
| this._metricsCache.refresh([false, ""]), | ||
| this._metricsCache.refresh([true, ""]), | ||
| this._metricsUserCountsCache.refresh([]), |
There was a problem hiding this comment.
Refresh or invalidate all metrics cache variants during user refresh
Line 574 and Line 575 only refresh the unfiltered keys, so previously-cached filtered entries can stay stale after mutations that call _refreshUsers (filtered metrics are used downstream in apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx).
Proposed fix
protected override async _refreshUsers() {
await Promise.all([
super._refreshUsers(),
- this._metricsCache.refresh([false, ""]),
- this._metricsCache.refresh([true, ""]),
+ this._metricsCache.invalidateWhere(() => true),
this._metricsUserCountsCache.refresh([]),
]);
+ await Promise.all([
+ this._metricsCache.refresh([false, ""]),
+ this._metricsCache.refresh([true, ""]),
+ ]);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| protected override async _refreshUsers() { | |
| await Promise.all([ | |
| super._refreshUsers(), | |
| this._metricsCache.refresh([false]), | |
| this._metricsCache.refresh([true]), | |
| this._metricsCache.refresh([false, ""]), | |
| this._metricsCache.refresh([true, ""]), | |
| this._metricsUserCountsCache.refresh([]), | |
| protected override async _refreshUsers() { | |
| await Promise.all([ | |
| super._refreshUsers(), | |
| this._metricsCache.invalidateWhere(() => true), | |
| this._metricsUserCountsCache.refresh([]), | |
| ]); | |
| await Promise.all([ | |
| this._metricsCache.refresh([false, ""]), | |
| this._metricsCache.refresh([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 `@packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts`
around lines 571 - 576, The _refreshUsers override currently only refreshes two
specific metric cache keys (this._metricsCache.refresh([false, ""]) and
this._metricsCache.refresh([true, ""])) which leaves other filtered/variant
entries stale; update _refreshUsers to invalidate or refresh all variants of the
metrics caches instead of just those two keys by iterating over or using a
wildcard/invalidate method on this._metricsCache and
this._metricsUserCountsCache so every cached filtered key is cleared/refreshed
when _refreshUsers runs (locate the override _refreshUsers and the calls to
this._metricsCache.refresh and this._metricsUserCountsCache.refresh to apply the
change).
Summary
Adds richer analytics overview metrics and filterable dashboard breakdowns.
Validation
Attempted targeted tests:
pnpm test run apps/backend/src/app/api/latest/internal/metrics/route.test.ts 'apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.test.ts'This did not reach Vitest in the temporary split worktree because
node_modulesis not installed there and the repo pre-step failed atpnpm exec tsx ./scripts/generate-sdks.ts.Summary by cubic
Adds analytics overview filters, 1‑day hourly charts, and richer breakdowns. Also polishes the UI with improved globe controls/layout, clearer tooltips, and smoother chart animations with reduced‑motion support.
New Features
user_agent; server stamps request UA when missing; UA‑based filters use field fragments (no UA allowlist).Refactors
getMetricsand the admin app cache.process.env.NEXT_RUNTIME === "nodejs"to avoid Edge imports in Edge builds.TooltipPortalfrom@stackframe/stack-ui; adoptedmotionfor smoother chart and sparkline animations.Written for commit a1fff45. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
Release Notes
New Features
Improvements