Skip to content

[codex] Add analytics overview filters#1496

Open
mantrakp04 wants to merge 7 commits into
devfrom
codex/analytics-overview-filters
Open

[codex] Add analytics overview filters#1496
mantrakp04 wants to merge 7 commits into
devfrom
codex/analytics-overview-filters

Conversation

@mantrakp04
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 commented May 27, 2026

Summary

Adds richer analytics overview metrics and filterable dashboard breakdowns.

  • adds hourly overview series for the 1-day range
  • adds country, referrer, browser, OS, and device filters to internal metrics
  • adds bounce rate, session duration, top countries, top browsers, top operating systems, and device breakdowns
  • updates the overview dashboard with filter chips, top-list cards, animated metric states, and 1-day hourly chart support
  • captures user agent on page-view analytics events, with a server-side fallback for older clients

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_modules is not installed there and the repo pre-step failed at pnpm 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

    • Filters: country, referrer, browser, OS, device across metrics API and dashboard; inputs normalized before queries.
    • 1‑day hourly series for page views, visitors, and clicks.
    • New metrics/breakdowns: bounce rate, average session duration, top countries, browsers, OS, device classes, and referrers.
    • UI: filter chips/menu, chart metric mode toggle (dau/visitors/revenue), animated top‑list cards, motion‑based chart/sparkline animations that respect reduced motion, smoother pill/tabs sliders, optional interactive globe with dynamic camera distance and layout fixes.
    • Tooltips: clearer metric descriptions (e.g., Unique Visitors, Avg. Session Time) and tooltip support on user metric cards.
    • User‑Agent: client sends user_agent; server stamps request UA when missing; UA‑based filters use field fragments (no UA allowlist).
    • Tests: filter normalization, UA filter fragments, chart‑mode toggle.
  • Refactors

    • Metrics fetch accepts filters; cache keys serialize filters across getMetrics and the admin app cache.
    • Replaced UA allowlist classification with filter‑fragment builder for analytics filters; updated tests.
    • Instrumentation guarded by process.env.NEXT_RUNTIME === "nodejs" to avoid Edge imports in Edge builds.
    • Exported TooltipPortal from @stackframe/stack-ui; adopted motion for 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

    • Added analytics filtering capabilities by country, referrer, browser, OS, and device.
    • Added hourly metrics for user signups and active users.
    • Added metric mode switching on analytics charts (DAU, Visitors, Revenue).
    • Added "Top Lists" cards displaying top referrers, regions, and devices.
    • Added page views visualization on analytics charts.
  • Improvements

    • Enhanced animations with reduced-motion accessibility support.
    • Improved user-agent data collection for analytics.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-auth-internal-tool Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-auth-mcp Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-auth-skills Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-backend Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-dashboard Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-demo Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-docs Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-preview-backend Ready Ready Preview, Comment May 27, 2026 10:58pm
stack-preview-dashboard Ready Ready Preview, Comment May 27, 2026 10:58pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Analytics Backend: Filtering, Hourly Metrics, and User-Agent Tracking

Layer / File(s) Summary
User-Agent Header Capture in Event Tracking
apps/backend/src/app/api/latest/analytics/events/batch/route.tsx, packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts
POST /events/batch handler now accepts full request to read User-Agent header; event data normalized to object type and conditionally injected with header user-agent when field missing/empty; EventTracker client includes user_agent from navigator.userAgent in $page-view events.
Analytics Filter Types and Normalization
apps/backend/src/app/api/latest/internal/metrics/route.tsx, apps/dashboard/src/lib/stack-app-internals.ts, packages/stack-shared/src/interface/admin-interface.ts
Introduces AnalyticsOverviewFilters type (country_code, referrer, browser, os, device) with normalizeAnalyticsOverviewFilters function; extends GET query validation for filter_* parameters; propagates filter types through dashboard internals and admin interface getMetrics method.
Hourly Metrics Computation
apps/backend/src/app/api/latest/internal/metrics/route.tsx
Adds loadHourlyUsers and loadHourlyActiveUsers helpers to compute hourly signup/active-user counts from ClickHouse for last 24 hours; extends response schema to include hourly_users and hourly_active_users as DataPoints.
Analytics Overview ClickHouse Query Refactor
apps/backend/src/app/api/latest/internal/metrics/route.tsx
Refactors loadAnalyticsOverview to accept filters and dynamically construct user-joins/filter fragments for country/referrer/browser/OS/device dimensions; updates query fan-out to include hourly page-view/active-user/visitor aggregation; computes bounce-rate/avg-session metrics; extends response with hourly aggregates, top-N lists, and engagement metrics with rollout-safe defaults.
Analytics Filter Normalization Tests
apps/backend/src/app/api/latest/internal/metrics/route.test.ts
Adds test cases verifying that analytics overview filters are normalized (trimming/uppercasing/empty→undefined) and user-agent filter fragments are deterministically built without raw allowlist usage.
Shared Metrics Types and Admin Interface
packages/stack-shared/src/interface/admin-metrics.ts, packages/stack-shared/src/interface/admin-interface.ts
Introduces MetricsNamedCountSchema and MetricsTopCountrySchema; extends MetricsAnalyticsOverviewSchema with hourly/bounce/session/top-list fields; exports derived types; updates HexclaveAdminInterface.getMetrics to accept optional filters and append filter_* query parameters.
Admin App Metrics Cache with Filter Support
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
Updates metrics cache key to include serialized filtersKey; parses filters from cache key and calls getMetrics with both includeAnonymous and filters; updates user refresh to clear cache for both anonymous/non-anonymous with empty filters.

Dashboard UI: Animated Components, Filters, and Layout Refactoring

Layer / File(s) Summary
Animated Slider Indicators for Pills and Tabs
packages/dashboard-ui-components/src/components/pill-toggle.tsx, packages/dashboard-ui-components/src/components/tabs.tsx
DesignPillToggle and DesignCategoryTabs measure selected button position/size via refs and ResizeObserver; render absolutely-positioned animated background indicators; respect prefers-reduced-motion for transition disabling; pill-toggle wraps in TooltipProvider when labels hidden.
Interactive Globe Mode
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe-section-with-data.tsx
GlobeSection accepts optional interactive prop (default false); when interactive, camera zoom enabled with configurable min/max distances; when not interactive, zoom disabled and distances locked; globe container gains overflow-hidden styling.
Standardized Chart Motion with Reduced-Motion Support
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx
Adds useChartMotionProps hook disabling animations when prefers-reduced-motion enabled; consolidates X-axis tick formatting via formatChartXAxisTick helper; updates ActivityBarChart, StackedBarChart, CorrelationChart, Donut, Email, Visitors, and Revenue cards to spread chartMotion instead of hard-coded animation settings; adds TimeRange '1d' option for 1-day view.
Page Views Data in Composed Charts
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx
ComposedAnalyticsChart extends data model with page_views and _showPageViews flags; tooltip adds "Page views" row; chart renders page_views bar with hover-highlight clipPath; TabbedMetricsCard gains chartDataIsPreFiltered and headerTooltip optional props.
Analytics Chart Metric Mode Toggle
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.ts, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.test.ts
Introduces AnalyticsChartMode type and ANALYTICS_CHART_METRIC_MODE_ORDER constant; adds toggleAnalyticsChartMetricMode helper; includes test coverage for toggle reset and selection behavior.
Top Lists Animated Cards Component
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx
New module exports useAnimatedBarValues hook for smooth proportional bar animation; utilities for referrer host/favicon/country flag/region name; three reusable cards: ReferrersWithAnalyticsCard (infinite scroll with visitor animation), TopRegionsCard (country breakdown with selection), and TopNamedListCard (generic top-N with parameterization).
User Page Metric Card with Sparkline Animation
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/user-page-metric-card.tsx
UserPageMetricCard accepts optional tooltip prop; Sparkline rewritten with derived geometry helpers, usePrefersReducedMotion hook, and useSparklineMotion for animated transform/opacity; SVG renders area/line paths wrapped in motion-styled group element.
Major Metrics Page Restructuring and Filter Integration
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
Adds local helpers for animation/icon rendering; implements full analytics filter UI (dimension definitions, chips bar, dropdown menu with preloaded unfiltered metrics); refactors AnalyticsInChartPill to toggle-driven semantics with keyboard/arrow navigation; rewrites composed chart data building to include hourly/page-view/revenue keyed by time range; computes period deltas and sparklines; replaces outer stats pills with UserPageMetricCard grid; replaces old top-N widgets with new TopRegionsCard/TopNamedListCard with filter wiring.

Shared Types, Exports, and Configuration

Layer / File(s) Summary
UI Library and Dashboard Re-Exports
packages/stack-ui/src/components/ui/tooltip.tsx, apps/dashboard/src/lib/stack-app-internals.ts
Exports TooltipPortal from tooltip.tsx; re-exports additional metrics types (MetricsNamedCount, MetricsTopCountry, MetricsResponse) from shared admin-metrics; exports new AnalyticsOverviewFilters type.
Runtime Configuration and Dependencies
apps/dashboard/src/instrumentation.ts, apps/dashboard/package.json, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/overview/page.tsx
Instrumentation gates Node-only startup on process.env.NEXT_RUNTIME; adds motion package (^12.39.0) dependency; creates analytics overview redirect page.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hexclave/stack-auth#1457: Adds OOM prevention (event_at >= since bound) to analytics user join in same ClickHouse route layer.
  • hexclave/stack-auth#1407: Modifies event batch endpoint request body decoding in same POST /events/batch route file.

Suggested reviewers

  • N2D4
  • nams1570
  • BilalG1

🐰 A rabbit hops through the dashboard,
watching metrics flow like streams of starlight—
hourly now, filtered clear,
with animations smooth and bright,
top regions dance, filters dance too,
the platform's heartbeat rings true! 🎨✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding analytics overview filters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering objectives, validation attempts, and high-level feature summaries with clear sectioning.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/analytics-overview-filters

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- 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.
mantrakp04 and others added 2 commits May 27, 2026 15:45
…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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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/metrics response + 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_agent on 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.

Comment on lines +1687 to +1693
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 });
Comment on lines +1418 to +1431
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,
Comment on lines +584 to +590
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;
Comment on lines +144 to +151
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const updatePrefersReducedMotion = () => setPrefersReducedMotion(mediaQuery.matches);

updatePrefersReducedMotion();
mediaQuery.addEventListener("change", updatePrefersReducedMotion);

return () => mediaQuery.removeEventListener("change", updatePrefersReducedMotion);
Comment on lines +95 to +102
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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 27, 2026

Greptile Summary

This 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 navigator.userAgent on page-view events client-side and falls back to the HTTP User-Agent header server-side so older clients still populate UA breakdowns.

  • Backend (metrics/route.tsx): Eight ClickHouse queries now run in parallel per metrics request; each dimension's top-N query intentionally omits its own filter so users can see alternate choices. Filter values are passed exclusively as named ClickHouse parameters — no interpolation, no injection risk.
  • Frontend (metrics-page.tsx, top-lists.tsx): New filter-chip UI, animated bar/sparkline cards, brand/flag icons loaded lazily from cdn.simpleicons.org and flagcdn.com. top-lists.tsx is a new file; easeOutCubic and prefersReducedMotion are duplicated from metrics-page.tsx and could be extracted to a shared utility.
  • Schema (admin-metrics.ts): All new response fields use .optional().default([]) / .default(0) so dashboards talking to older servers degrade gracefully without breaking validation.

Confidence Score: 4/5

Safe 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 setMinutes / setUTCMinutes inconsistency in loadAnalyticsOverview would silently blank out the hourly chart series on any server running in a non-UTC timezone, but standard cloud/Docker deployments default to UTC so it is unlikely to affect production today. Utility code duplication across the two new frontend files is a quality concern but does not affect correctness.

apps/backend/src/app/api/latest/internal/metrics/route.tsx — the hourly output loop at line 1688 uses setMinutes where every other hourly function in the same file uses setUTCMinutes.

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/internal/metrics/route.tsx Major expansion adding hourly series, UA-based breakdowns, and per-dimension filter logic; all ClickHouse params are properly named to avoid injection. One inconsistency: uses setMinutes instead of setUTCMinutes for the hourly output loop boundary, which would produce a misaligned 24-hour window on non-UTC servers.
apps/backend/src/app/api/latest/analytics/events/batch/route.tsx Adds server-side User-Agent stamping from request.headers["user-agent"] when the client doesn't send one; UA is stored in the JSON event-data blob and retrieved via typed ClickHouse accessor — no injection risk. Array header case is handled.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx Large rewrite adding filter chips, animated metric cards, 1-day hourly chart support, and brand/flag icon loading from third-party CDNs (simpleicons, flagcdn). Utility functions (easeOutCubic, prefersReducedMotion) are duplicated into top-lists.tsx.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx New file providing referrer, region, and named-count breakdown cards with animated bar charts. Duplicates easeOutCubic/prefersReducedMotion from metrics-page.tsx; otherwise logic is clean.
packages/stack-shared/src/interface/admin-metrics.ts Adds new schema types for UA breakdowns, top countries, hourly series, and bounce/session metrics. All new fields are .optional().default([]) / .default(0) for forward compatibility with older servers — well-guarded migration pattern.
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts Cache key serializes filters via JSON.stringify so filter objects with equal contents share a cache entry. _refreshUsers still only invalidates the no-filter cache entries, which is the intended behaviour — filtered views expire naturally.
apps/dashboard/src/instrumentation.ts Replaces getNextRuntime() === "nodejs" guard with process.env.NEXT_RUNTIME === "nodejs" to prevent the Edge bundle from following Node-only dynamic imports — a correct fix for the Edge/Node split.
packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts Adds user_agent: navigator.userAgent to page-view event payloads with a proper SSR guard; pairs with the server-side fallback in the batch route.

Sequence Diagram

sequenceDiagram
    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, ...)
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
Fix 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

Comment on lines +1687 to +1688
const latestHour = new Date(now);
latestHour.setMinutes(0, 0, 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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.

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +17 to +23
}

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> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Fix in Claude Code Fix in Cursor Fix in Codex

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";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
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

Fix on Vercel

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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))
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 27, 2026

Choose a reason for hiding this comment

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

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>
Suggested change
const baseData = (typeof event.data === "object" && !Array.isArray(event.data))
const baseData = (event.data != null && typeof event.data === "object" && !Array.isArray(event.data))
Fix with Cubic

const hourlyActiveUsers: DataPoints = [];
const hourlyVisitors: DataPoints = [];
const latestHour = new Date(now);
latestHour.setMinutes(0, 0, 0);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 27, 2026

Choose a reason for hiding this comment

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

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>
Suggested change
latestHour.setMinutes(0, 0, 0);
latestHour.setUTCMinutes(0, 0, 0);
Fix with Cubic

Comment on lines +118 to +126
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()}`;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 27, 2026

Choose a reason for hiding this comment

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

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>
Suggested change
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()}`;
}
Fix with Cubic

controls.enableZoom = true;
controls.minDistance = cameraDistance;
controls.maxDistance = 600;
} else {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 27, 2026

Choose a reason for hiding this comment

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

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>
Suggested change
} else {
} else {
controls.enableZoom = false;
Fix with Cubic

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) : "";
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 27, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic


export function regionName(code: string): string {
try {
const dn = new Intl.DisplayNames([typeof navigator !== "undefined" ? navigator.language : "en"], { type: "region" });
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 27, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

ORDER BY hour ASC
`,
query_params: {
hourlySince: formatClickhouseDateTimeParam(new Date(now.getTime() - 23 * 60 * 60 * 1000)),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 27, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx (1)

112-113: ⚡ Quick win

Use guideline-compliant URL construction for remote image sources.

These URLs are currently built with raw interpolation; please switch to urlString or explicit encoding for consistency with project standards.

As per coding guidelines, "Use urlString`` or encodeURIComponent()` 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

📥 Commits

Reviewing files that changed from the base of the PR and between c0fefd3 and a1fff45.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • apps/backend/src/app/api/latest/analytics/events/batch/route.tsx
  • apps/backend/src/app/api/latest/internal/metrics/route.test.ts
  • apps/backend/src/app/api/latest/internal/metrics/route.tsx
  • apps/dashboard/package.json
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.test.ts
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.ts
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe-section-with-data.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/overview/page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/user-page-metric-card.tsx
  • apps/dashboard/src/instrumentation.ts
  • apps/dashboard/src/lib/stack-app-internals.ts
  • packages/dashboard-ui-components/src/components/pill-toggle.tsx
  • packages/dashboard-ui-components/src/components/tabs.tsx
  • packages/stack-shared/src/interface/admin-interface.ts
  • packages/stack-shared/src/interface/admin-metrics.ts
  • packages/stack-ui/src/components/ui/tooltip.tsx
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts

Comment on lines +137 to +149
// 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>)
: {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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 || true

Repository: 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
fi

Repository: hexclave/stack-auth

Length of output: 7351


Remove as casts and fail fast on non-object event.data in analytics batch ingestion

  • headerUserAgent uses (raw as string | undefined), and event.data is cast to Record<string, unknown> via as and then silently coerced to {} when it’s not an object. This hides malformed client payloads (the request schema uses yupMixed() for data) and can skew analytics.
  • Replace both casts with type guards and return StatusError.BadRequest when event.data isn’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.

Comment on lines +1428 to +1430
hourlySince: formatClickhouseDateTimeParam(new Date(now.getTime() - 23 * 60 * 60 * 1000)),
since: formatClickhouseDateTimeParam(since),
untilExclusive: formatClickhouseDateTimeParam(untilExclusive),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +372 to +393
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]);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +260 to +264
{tooltip == null ? labelNode : (
<SimpleTooltip tooltip={tooltip} inline className="w-fit">
{labelNode}
</SimpleTooltip>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
{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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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" . || true

Repository: 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" . || true

Repository: 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 || true

Repository: 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 || true

Repository: 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" || true

Repository: 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.

Comment on lines +123 to +127
const resizeObserver = new ResizeObserver(updateSliderMetrics);
resizeObserver.observe(toggle);
resizeObserver.observe(selectedButton);

return () => resizeObserver.disconnect();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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 jsx

Repository: 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)" . || true

Repository: 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 . || true

Repository: 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 . || true

Repository: 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*' || true

Repository: 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" . || true

Repository: 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 . || true

Repository: 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 . || true

Repository: 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.

Suggested change
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.

Comment on lines +172 to +177
const resizeObserver = new ResizeObserver(updateSliderMetrics);
resizeObserver.observe(tabList);
resizeObserver.observe(selectedButton);

return () => resizeObserver.disconnect();
}, [categories, selectedCategory]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all unguarded ResizeObserver usages in UI components.
rg -n "new ResizeObserver\\(" --type ts --type tsx

Repository: 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' . || true

Repository: 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' . || true

Repository: 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' . || true

Repository: 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
done

Repository: 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' . || true

Repository: 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' . || true

Repository: 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' . || true

Repository: 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 || true

Repository: 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" || true

Repository: 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.

Comment on lines 571 to 576
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([]),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants