Skip to content

Conversation

@Vermino
Copy link

@Vermino Vermino commented Nov 19, 2025

Backend Enhancements (server/src/api/sites/getSitesFromOrg.ts)

  • Added optional includeMetrics and timePeriod query parameters
  • Fetches comprehensive analytics for all sites in one query:
  • Users, Sessions, Pageviews
  • Bounce Rate, Session Duration, Pages per Session
  • Period-over-period comparisons (current vs previous period)
  • Supports three time periods: 24h, 7d, 30d

Frontend Components

  1. SitesOverviewTable - Comprehensive table view featuring:
  • Sortable columns for all metrics
  • Visual trend indicators (↑/↓) with color coding
  • Percentage change from previous period
  • Totals row showing aggregate statistics
  • Click domain to navigate to detailed analytics
  1. SitesSummaryStats - High-level overview cards showing:
  • Total Sites
  • Total Users, Sessions, Pageviews
  • Average Bounce Rate & Session Duration
  • Trend indicators comparing to previous period
  1. Enhanced Home Page with:
  • View Toggle: Switch between Cards and Table views
  • Time Period Selector: Choose 24h, 7d, or 30d
  • Smart Data Fetching: Only loads detailed metrics when in table view
  • Persistent Preferences: Saves your view mode and time period in localStorage

Summary by CodeRabbit

  • New Features
    • Added table and card view options for sites, with preference saved automatically
    • Added time period selector to analyze metrics over 24 hours, 7 days, or 30 days
    • Added detailed metrics table with sortable columns for users, sessions, pageviews, bounce rate, and session duration
    • Added summary statistics card displaying aggregate metrics across all sites

Vermino and others added 2 commits November 18, 2025 20:12
…rypoint.sh is executable

Prevents 'exec /docker-entrypoint.sh: no such file or directory' on Windows checkouts by stripping CRLF (sed) and chmod +x in the runtime image.
… stats

This commit implements a comprehensive 30,000-foot view of all monitored domains, enabling users to see all site metrics at a glance.

## Backend Changes
- Enhanced `getSitesFromOrg` API endpoint to support optional detailed metrics
- Added query parameters: `includeMetrics` (boolean) and `timePeriod` (24h/7d/30d)
- Implemented comprehensive analytics aggregation across all sites including:
  - Users, Sessions, Pageviews
  - Bounce Rate, Session Duration, Pages per Session
  - Period-over-period comparison calculations
- Optimized queries to only fetch detailed metrics when requested

## Frontend Changes
- Created `SitesOverviewTable` component with:
  - Sortable columns for all key metrics
  - Period-over-period change indicators with color coding
  - Totals row showing aggregate statistics
  - Responsive design with horizontal scroll
- Created `SitesSummaryStats` component displaying:
  - Total sites, users, sessions, pageviews
  - Average bounce rate and session duration
  - Trend indicators comparing to previous period
- Enhanced home page with:
  - View toggle between Cards and Table modes
  - Time period selector (24h, 7d, 30d)
  - LocalStorage persistence for user preferences
  - Conditional data fetching based on view mode

## Type Updates
- Added `SiteMetrics` type with all metric fields and change percentages
- Updated `GetSitesFromOrgResponse` to include optional metrics
- Modified `useGetSitesFromOrg` hook to accept options for metrics and time period

## Features
- One-click switching between card and table views
- Flexible time period selection with period-over-period comparison
- Sortable table columns for easy data analysis
- Visual indicators for metric trends (up/down arrows with color coding)
- Total aggregation across all sites for quick overview
- Persistent user preferences across sessions
@vercel
Copy link

vercel bot commented Nov 19, 2025

@claude is attempting to deploy a commit to the goldflag's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Walkthrough

This PR adds metrics retrieval and multi-view display for sites. The backend now supports optional query parameters (includeMetrics, timePeriod) to fetch detailed site metrics over configurable periods. The frontend extends the sites hook to support these parameters, adds client-side state for view mode and time period selection with localStorage persistence, and introduces two new components for tabular metrics display and aggregate statistics. A Dockerfile line-ending normalization is also included.

Changes

Cohort / File(s) Summary
Backend API
server/src/api/sites/getSitesFromOrg.ts
Added includeMetrics and timePeriod query parameters; implements metrics fetching for current and previous periods; calculates per-site metric changes; augments response with optional metrics field containing deltas.
Frontend API Hook
client/src/api/admin/sites.ts
Introduced SiteMetrics type; extended useGetSitesFromOrg to accept options with includeMetrics and timePeriod; updated queryKey and queryFn to include these parameters in URL.
New Metrics Components
client/src/components/SitesOverviewTable.tsx, client/src/components/SitesSummaryStats.tsx
SitesOverviewTable: sortable, paginated table of site metrics with per-site rows and totals row. SitesSummaryStats: aggregated statistics grid with trend indicators and loading states.
Page Integration
client/src/app/page.tsx
Added client-side state for viewMode ("cards"|"table") and timePeriod; persisted to localStorage; added UI controls for view switching and time period selection; conditionally renders cards or table view with appropriate metrics fetching.
Infrastructure
server/Dockerfile
Normalized CRLF line endings on docker-entrypoint.sh using sed, then applied chmod +x.

Sequence Diagram

sequenceDiagram
    participant User
    participant Page as page.tsx
    participant Hook as useGetSitesFromOrg
    participant Backend as getSitesFromOrg
    participant DB as Database

    User->>Page: Select time period / view mode
    Note over Page: Persist state to localStorage
    Page->>Hook: Call with includeMetrics & timePeriod
    Hook->>Backend: GET /sites?includeMetrics=true&timePeriod=7d
    Backend->>DB: Fetch sites & ownership
    alt includeMetrics = true
        Backend->>DB: Fetch metrics (current period)
        Backend->>DB: Fetch metrics (previous period)
        Note over Backend: Calculate deltas (usersChange, etc.)
    end
    Backend-->>Hook: Return sites with metrics
    Hook-->>Page: Update query cache
    alt viewMode = "table"
        Page->>SitesOverviewTable: Render with metrics
        Page->>SitesSummaryStats: Render aggregated stats
    else viewMode = "cards"
        Page->>SiteCard: Render site cards
    end
    SitesOverviewTable-->>User: Sortable table + totals
    SitesSummaryStats-->>User: Summary stats grid
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

  • Metrics calculation logic in getSitesFromOrg.ts: verify correctness of time-period logic, delta calculations, and concurrent data fetching
  • Sorting and aggregation in SitesOverviewTable.tsx: confirm sort behavior, numeric vs. string handling, totals row computation
  • State persistence in page.tsx: validate localStorage integration, client-only flag, and hook dependency chain
  • Query key dependencies: ensure includeMetrics and timePeriod are properly cached and invalidated
  • Type consistency: verify SiteMetrics type flows correctly between client hook, backend response, and components

Poem

🐰✨ A rabbit hops through metrics bright,
Cards and tables, day and night,
Time periods span, deltas dance,
Stats aggregate—give them a glance!
Summaries bloom in localStorage deep,
While sorted rows their order keep. 🌿

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title "Claude/add quick overview" is vague and generic, using non-descriptive branch naming (prefix + non-specific term) that doesn't convey the actual changeset purpose. Use a more descriptive title that reflects the main feature: e.g., "Add quick overview dashboard with metrics table and time period filtering" or "Add analytics overview with table view and metrics comparison."
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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.

@Vermino Vermino marked this pull request as draft November 19, 2025 06:26
@Vermino Vermino closed this Nov 19, 2025
Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/api/sites/getSitesFromOrg.ts (1)

62-223: Session-level metrics (bounce rate, pages/session, session duration) are computed incorrectly over events instead of sessions

In both the current and previous period metrics queries, you:

  • Build AllSessionPageviews and SessionStats (one row per site+session with total_pageviews_in_session, start_time, end_time).
  • Then do FROM events e LEFT JOIN SessionStats ss USING (site_id, session_id) and aggregate:
AVG(total_pageviews_in_session)                 AS pages_per_session,
sumIf(1, total_pageviews_in_session = 1)
/ COUNT(DISTINCT session_id) * 100             AS bounce_rate,
AVG(end_time - start_time)                     AS session_duration

Because the join duplicates each SessionStats row once per event in that session:

  • AVG(total_pageviews_in_session) and AVG(end_time - start_time) become event‑weighted, not session‑weighted averages.
  • sumIf(1, total_pageviews_in_session = 1) counts one per event in a “bounce” session (not one per session), so if a bounce session has multiple non‑pageview events, the numerator can exceed COUNT(DISTINCT session_id) and bounce_rate can go over 100%.

This significantly distorts the metrics the UI relies on.

A safer pattern is to:

  1. Aggregate once per session in SessionStats.
  2. Compute session‑level aggregates (sessions count, pages/session, bounce rate, session duration) in a separate CTE over SessionStats.
  3. Join that with an events‑level aggregate only for things that must be per‑event (users, pageviews).

Sketch:

-        const metricsResult = await clickhouse.query({
-          query: `
-            WITH
-            AllSessionPageviews AS (...),
-            SessionStats AS (...)
-            SELECT
-              site_id,
-              COUNT(DISTINCT session_id) AS sessions,
-              COUNT(DISTINCT CASE WHEN type = 'pageview' THEN session_id END) AS pageview_sessions,
-              uniqExact(user_id) AS users,
-              countIf(type = 'pageview') AS pageviews,
-              AVG(total_pageviews_in_session) AS pages_per_session,
-              sumIf(1, total_pageviews_in_session = 1) / COUNT(DISTINCT session_id) * 100 AS bounce_rate,
-              AVG(end_time - start_time) AS session_duration
-            FROM events e
-            LEFT JOIN SessionStats ss USING (site_id, session_id)
-            WHERE
-              site_id IN (${siteIds.join(",")})
-              AND timestamp >= now() - INTERVAL ${currentInterval}
-            GROUP BY site_id
-          `,
-          format: "JSONEachRow",
-        });
+        const metricsResult = await clickhouse.query({
+          query: `
+            WITH
+            AllSessionPageviews AS (...),
+            SessionStats AS (...),
+            SessionAgg AS (
+              SELECT
+                site_id,
+                count() AS sessions,
+                avg(total_pageviews_in_session) AS pages_per_session,
+                sumIf(total_pageviews_in_session = 1, 1) / count() * 100 AS bounce_rate,
+                avg(end_time - start_time) AS session_duration
+              FROM SessionStats
+              GROUP BY site_id
+            ),
+            EventAgg AS (
+              SELECT
+                site_id,
+                uniqExact(user_id) AS users,
+                countIf(type = 'pageview') AS pageviews
+              FROM events
+              WHERE
+                site_id IN (${siteIds.join(",")})
+                AND timestamp >= now() - INTERVAL ${currentInterval}
+              GROUP BY site_id
+            )
+            SELECT
+              ea.site_id,
+              sa.sessions,
+              ea.users,
+              ea.pageviews,
+              sa.pages_per_session,
+              sa.bounce_rate,
+              sa.session_duration
+            FROM SessionAgg sa
+            LEFT JOIN EventAgg ea USING (site_id)
+          `,
+          format: "JSONEachRow",
+        });

You’d apply the same pattern to the previous‑period query. Exact SQL may need tweaking, but the key point is: do session‑level metrics purely from one‑row‑per‑session data, and only join in events for distinct users/pageviews.

🧹 Nitpick comments (4)
client/src/components/SitesSummaryStats.tsx (2)

15-69: Averages and % changes are biased by including sites without metrics in the denominator

totals.count is incremented for every site, while metrics fields use (site.metrics?....) ?? 0. For sites without metrics this effectively treats “no data” as 0, so:

  • avgBounceRate, avgSessionDuration, avgPagesPerSession
  • usersChange, sessionsChange, pageviewsChange

are all pulled down if some sites have no metrics yet.

If the intent is “averages across sites with data”, a more accurate approach is to only increment the counter when site.metrics is present.

Example change:

-    const totals = sites.reduce(
-      (acc, site) => ({
-        users: acc.users + (site.metrics?.users ?? 0),
-        sessions: acc.sessions + (site.metrics?.sessions ?? 0),
-        pageviews: acc.pageviews + (site.metrics?.pageviews ?? 0),
-        bounceRate: acc.bounceRate + (site.metrics?.bounceRate ?? 0),
-        sessionDuration: acc.sessionDuration + (site.metrics?.sessionDuration ?? 0),
-        pagesPerSession: acc.pagesPerSession + (site.metrics?.pagesPerSession ?? 0),
-        usersChange: acc.usersChange + (site.metrics?.usersChange ?? 0),
-        sessionsChange: acc.sessionsChange + (site.metrics?.sessionsChange ?? 0),
-        pageviewsChange: acc.pageviewsChange + (site.metrics?.pageviewsChange ?? 0),
-        count: acc.count + 1,
-      }),
+    const totals = sites.reduce(
+      (acc, site) => {
+        const m = site.metrics;
+        if (!m) return acc;
+        return {
+          users: acc.users + m.users,
+          sessions: acc.sessions + m.sessions,
+          pageviews: acc.pageviews + m.pageviews,
+          bounceRate: acc.bounceRate + m.bounceRate,
+          sessionDuration: acc.sessionDuration + m.sessionDuration,
+          pagesPerSession: acc.pagesPerSession + m.pagesPerSession,
+          usersChange: acc.usersChange + m.usersChange,
+          sessionsChange: acc.sessionsChange + m.sessionsChange,
+          pageviewsChange: acc.pageviewsChange + m.pageviewsChange,
+          count: acc.count + 1,
+        };
+      },
       {
         users: 0,
         sessions: 0,
         pageviews: 0,
         bounceRate: 0,
         sessionDuration: 0,
         pagesPerSession: 0,
         usersChange: 0,
         sessionsChange: 0,
         pageviewsChange: 0,
         count: 0,
       }
     );

You’d keep totalSites: sites.length as-is so the card still shows total sites irrespective of metrics.


72-84: Tighten StatCard icon typing for better safety and DX

icon is currently typed as any, which loses type safety and editor help. You can narrow this to the Lucide/React component shape you actually use:

-  const StatCard = ({
-    icon: Icon,
+  const StatCard = ({
+    icon: Icon,
@@
-  }: {
-    icon: any;
+  }: {
+    icon: React.ComponentType<{ size?: number; className?: string }>;
     label: string;
     value: string | number;
     change?: number;
     suffix?: string;
   }) => (

(With a corresponding import type React from "react"; or using the existing React import pattern in the project.)

client/src/components/SitesOverviewTable.tsx (1)

102-133: Totals row averages are skewed by including sites without metrics

In the totals calculation, count is incremented for every site, while metrics use (site.metrics?....) ?? 0. This means sites with no metrics are effectively treated as zeroes when computing:

  • avgSessionDuration
  • avgPagesPerSession
  • avgBounceRate

If you want “averages across sites with data”, the denominator should be the number of sites that have metrics, not the total number of sites.

You can mirror the pattern suggested for SitesSummaryStats:

-    const total = sites.reduce(
-      (acc, site) => ({
-        users: acc.users + (site.metrics?.users ?? 0),
-        sessions: acc.sessions + (site.metrics?.sessions ?? 0),
-        pageviews: acc.pageviews + (site.metrics?.pageviews ?? 0),
-        sessionDuration: acc.sessionDuration + (site.metrics?.sessionDuration ?? 0),
-        pagesPerSession: acc.pagesPerSession + (site.metrics?.pagesPerSession ?? 0),
-        bounceRate: acc.bounceRate + (site.metrics?.bounceRate ?? 0),
-        count: acc.count + 1,
-      }),
+    const total = sites.reduce(
+      (acc, site) => {
+        const m = site.metrics;
+        if (!m) return acc;
+        return {
+          users: acc.users + m.users,
+          sessions: acc.sessions + m.sessions,
+          pageviews: acc.pageviews + m.pageviews,
+          sessionDuration: acc.sessionDuration + m.sessionDuration,
+          pagesPerSession: acc.pagesPerSession + m.pagesPerSession,
+          bounceRate: acc.bounceRate + m.bounceRate,
+          count: acc.count + 1,
+        };
+      },
       {
         users: 0,
         sessions: 0,
         pageviews: 0,
         sessionDuration: 0,
         pagesPerSession: 0,
         bounceRate: 0,
         count: 0,
       }
     );

This keeps the totals row aligned with how you compute averages in SitesSummaryStats.

client/src/app/page.tsx (1)

28-39: Consider validating persisted viewMode / timePeriod values from localStorage

localStorage.getItem(...) values are cast to ViewMode / TimePeriod and applied directly if truthy. If those keys ever get corrupted (e.g. manual edits, old values), state could end up with an invalid string and the UI would still try to use it.

You could defensively guard the assignments:

  useEffect(() => {
    setIsClient(true);
-    const savedViewMode = localStorage.getItem("sitesViewMode") as ViewMode;
-    const savedTimePeriod = localStorage.getItem("sitesTimePeriod") as TimePeriod;
-    if (savedViewMode) setViewMode(savedViewMode);
-    if (savedTimePeriod) setTimePeriod(savedTimePeriod);
+    const savedViewMode = localStorage.getItem("sitesViewMode");
+    if (savedViewMode === "cards" || savedViewMode === "table") {
+      setViewMode(savedViewMode);
+    }
+    const savedTimePeriod = localStorage.getItem("sitesTimePeriod");
+    if (savedTimePeriod === "24h" || savedTimePeriod === "7d" || savedTimePeriod === "30d") {
+      setTimePeriod(savedTimePeriod as TimePeriod);
+    }
  }, []);

Not critical, but it makes the state more resilient to bad persisted values.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba9a3a7 and 1da9888.

📒 Files selected for processing (6)
  • client/src/api/admin/sites.ts (3 hunks)
  • client/src/app/page.tsx (2 hunks)
  • client/src/components/SitesOverviewTable.tsx (1 hunks)
  • client/src/components/SitesSummaryStats.tsx (1 hunks)
  • server/Dockerfile (1 hunks)
  • server/src/api/sites/getSitesFromOrg.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
client/src/api/admin/sites.ts (1)
client/src/api/utils.ts (1)
  • authedFetch (54-96)
client/src/components/SitesOverviewTable.tsx (3)
client/src/api/admin/sites.ts (1)
  • GetSitesFromOrgResponse (46-82)
client/src/components/Favicon.tsx (1)
  • Favicon (4-29)
client/src/lib/dateTimeUtils.ts (1)
  • formatDuration (75-97)
client/src/components/SitesSummaryStats.tsx (2)
client/src/api/admin/sites.ts (1)
  • GetSitesFromOrgResponse (46-82)
client/src/lib/dateTimeUtils.ts (1)
  • formatDuration (75-97)
client/src/app/page.tsx (3)
client/src/api/admin/sites.ts (1)
  • useGetSitesFromOrg (84-103)
client/src/components/SitesSummaryStats.tsx (1)
  • SitesSummaryStats (14-151)
client/src/components/SitesOverviewTable.tsx (1)
  • SitesOverviewTable (34-333)
server/src/api/sites/getSitesFromOrg.ts (2)
server/src/db/clickhouse/clickhouse.ts (1)
  • clickhouse (4-8)
server/src/api/analytics/utils.ts (1)
  • processResults (67-85)
🔇 Additional comments (2)
server/Dockerfile (1)

43-45: Excellent defensive fix for cross-platform line endings.

Normalizing CRLF to LF before execution is a solid best practice. This ensures the entrypoint script executes reliably on Alpine Linux containers regardless of the platform where the code was checked out. The sed command is clean, the && chaining is correct, and the updated comment clearly documents the intent.

client/src/api/admin/sites.ts (1)

31-44: Client metrics types and query options look consistent with the server

SiteMetrics matches the fields populated in the API, metrics?: SiteMetrics is correctly optional on each site, and useGetSitesFromOrg’s includeMetrics / timePeriod options are wired into both the queryKey and the URL params with sensible defaults, so existing call sites keep 24h behavior unless they opt in.

Also applies to: 72-99

@Vermino Vermino deleted the claude/add-quick-overview-01RpzwWPCZku5FLy4mdhRvPV branch November 19, 2025 09:16
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