Skip to content

Conversation

@richiemcilroy
Copy link
Member

@richiemcilroy richiemcilroy commented Nov 13, 2025

This PR implements Cap analytics.

YOLO. God speed.

Summary by CodeRabbit

  • New Features

    • Added comprehensive analytics dashboard with metrics, charts, and breakdowns
    • Implemented time-range selection (24h, 7d, 30d) for analytics filtering
    • Added video comparison feature with drag-and-drop support
    • Integrated geographic, browser, OS, and device analytics
    • Added real-time analytics tracking on video views
  • Documentation

    • Added analytics setup and environment configuration guide
  • Chores

    • Migrated analytics infrastructure to Tinybird
    • Updated environment variables (TINYBIRD_HOST, TINYBIRD_TOKEN)
    • Added analytics management CLI scripts

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 13, 2025

Walkthrough

This PR introduces a comprehensive analytics system powered by Tinybird, replacing the previous DUB-based integration. It adds new analytics dashboard components, server-side analytics tracking, Tinybird service infrastructure, database schema and setup scripts, refactors existing analytics flags across the codebase, and includes new animated UI components for the dashboard.

Changes

Cohort / File(s) Summary
Tinybird Infrastructure
packages/web-backend/src/Tinybird/index.ts, packages/web-backend/src/index.ts, packages/env/server.ts
New Tinybird service implementation with request/response handling, TSV parsing, event append and SQL query capabilities; exports TinybirdResponse and TinybirdEventRow types; adds TINYBIRD_HOST and TINYBIRD_TOKEN env variables.
Analytics API Routes
apps/web/app/api/analytics/route.ts, apps/web/app/api/analytics/track/route.ts, apps/web/app/api/dashboard/analytics/route.ts
New/updated API routes: analytics range support with date normalization; page-hit tracking endpoint accepting rich tracking payloads with user agent parsing and Tinybird ingestion; org analytics dashboard endpoint with org/space/range filtering.
Analytics Data Layer
apps/web/app/(org)/dashboard/analytics/data.ts, apps/web/app/(org)/dashboard/analytics/types.ts, apps/web/actions/videos/get-analytics.ts
New analytics response types (AnalyticsRange, OrgAnalyticsResponse, BreakdownRow); getOrgAnalyticsData orchestrating database and Tinybird queries with fallback logic; updated getVideoAnalytics with configurable date range and tenant-scoped querying.
Analytics Dashboard Components
apps/web/app/(org)/dashboard/analytics/components/*
New dashboard UI: AnalyticsDashboard, Header (org/space/range selection), StatsChart (metric boxes and area chart), ChartArea (Recharts integration), OtherStats (geographies, software, devices, caps tables), TableCard (data rows with icons), VideoFilters/VideosPicker (drag-drop video selection), FiltersList.
Animated Icon Components
apps/web/app/(org)/dashboard/_components/AnimatedIcons/ChartLine.tsx, apps/web/app/(org)/dashboard/_components/AnimatedIcons/Clap.tsx, apps/web/app/(org)/dashboard/_components/AnimatedIcons/Reaction.tsx, apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts
New motion/react-based animated icon components (ChartLineIcon, ClapIcon, PartyPopperIcon) with imperative control via forwardRef and useImperativeHandle; exported alongside existing icons.
Dashboard Navigation & Layout
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx, apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx, apps/web/app/(org)/dashboard/analytics/page.tsx
Added Analytics nav item with ChartLineIcon and matchChildren prefix matching; updated isPathActive with matchChildren logic; new titles map for analytics and folder paths with dynamic params; new analytics page entry point.
Caps/Video Analytics Refactoring
apps/web/app/(org)/dashboard/caps/Caps.tsx, apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx, apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx, apps/web/app/(org)/dashboard/caps/page.tsx
Renamed dubApiKeyEnabled to analyticsEnabled throughout; added per-video analytics and isLoadingAnalytics state; new RPC-based batch/single deletion mutations; CapCard UI refactor with dropdown menu for actions (analytics, settings, copy, etc.); analytics metrics link to dashboard.
Folder/Space Analytics
apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx, apps/web/app/(org)/dashboard/folder/[id]/page.tsx, apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx, apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx, apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx
Renamed dubApiKeyEnabled to analyticsEnabled with Tinybird-based env check (TINYBIRD_TOKEN && TINYBIRD_HOST).
Video View Tracking
apps/web/app/s/[videoId]/Share.tsx, apps/web/app/s/[videoId]/page.tsx
Added client-side analytics session management with localStorage persistence; trackVideoView POSTs to /api/analytics/track; added viewerId prop to Share and AuthorizedContent for tracking context.
UI Enhancements
apps/web/components/Tooltip.tsx, apps/web/components/ui/chart.tsx, packages/ui/src/components/Button.tsx, packages/ui/src/components/Select.tsx
Optional delayDuration prop on Tooltip; new ChartContainer/ChartTooltip/ChartLegend Recharts integration with theming; Button white variant styling updates (bg-gray-3, hover:bg-gray-6); Select component variant system with CVA and icon support.
Backend Videos Service
packages/web-backend/src/Videos/index.ts, packages/web-backend/src/Videos/VideosRpcs.ts, apps/web/lib/Queries/Analytics.ts
Added getAnalyticsBulk with Tinybird integration for batch analytics retrieval; getAnalytics delegates to bulk function; updated VideosGetAnalytics RPC to use bulk approach with per-video error handling; useVideosAnalyticsQuery uses Effect.flatMap with dataloader and batching.
Setup & Configuration
scripts/analytics/shared.js, scripts/analytics/setup-analytics.js, scripts/analytics/check-analytics.js, scripts/analytics/delete-all-data.js, scripts/analytics/populate-test-data.js
New analytics utility module with Tinybird client, auth handling, schema definitions; setup script automating Tinybird CLI, login, and project deployment; validation, deletion, and test data population scripts.
Tinybird Datasources & Pipes
scripts/analytics/tinybird/datasources/analytics_events.datasource, scripts/analytics/tinybird/datasources/analytics_pages_mv.datasource, scripts/analytics/tinybird/datasources/analytics_sessions_mv.datasource, scripts/analytics/tinybird/pipes/analytics_pages_mv_pipe.pipe, scripts/analytics/tinybird/pipes/analytics_sessions_mv_pipe.pipe
New Tinybird schema definitions: raw analytics_events table with MergeTree engine; aggregated pages materialized view with AggregatingMergeTree; session-level denormalization with ReplacingMergeTree; pipe nodes for aggregating views and denormalizing sessions.
Dependencies & Configuration
apps/web/package.json, package.json, .gitignore, README.md, apps/web/app/globals.css, apps/web/app/layout.tsx, apps/web/lib/server.ts, packages/database/auth/auth-options.ts, apps/web/app/(org)/dashboard/spaces/browse/page.tsx
Added recharts, country-flag-icons, ua-parser-js, @dnd-kit/core, @dnd-kit/utilities, mysql2; analytics npm scripts for migrate, setup, check, delete, populate; .tinyb gitignore patterns; removed DUB Analytics from layout; updated server deps with Tinybird; removed unused imports.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant Share as Share.tsx
    participant Track as /api/analytics/track
    participant Tinybird
    
    User->>Browser: Visits shared video
    Browser->>Share: Component mounts
    Share->>Share: ensureAnalyticsSessionId()
    Share->>Browser: Store session in localStorage
    Share->>Track: POST trackVideoView payload<br/>(videoId, sessionId, device, etc.)
    Track->>Track: Parse request & auth
    Track->>Track: Extract user-agent, location, etc.
    Track->>Tinybird: Append page_hit event
    Tinybird-->>Track: Success
    Track-->>Browser: { success: true }
Loading
sequenceDiagram
    participant Dashboard as AnalyticsDashboard
    participant API as /api/dashboard/analytics
    participant Backend as getOrgAnalyticsData
    participant DB as Database
    participant TB as Tinybird
    
    Dashboard->>API: GET /api/dashboard/analytics?orgId=X&range=7d&spaceId=Y
    API->>API: Validate auth, org, range
    API->>Backend: Call getOrgAnalyticsData(X, '7d', Y)
    Backend->>DB: Fetch videos, comments, reactions
    Backend->>TB: queryPipe / querySql for views, geographies, devices
    TB-->>Backend: Aggregated results
    Backend->>Backend: Normalize, aggregate, compute breakdowns
    Backend-->>API: OrgAnalyticsResponse
    API-->>Dashboard: { counts, chart, breakdowns, topCaps }
    Dashboard->>Dashboard: Render charts & tables
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Areas requiring extra attention:
    • Tinybird query building, SQL escaping, and fallback logic in apps/web/actions/videos/get-analytics.ts and apps/web/app/(org)/dashboard/analytics/data.ts — ensure data consistency and error handling
    • Analytics data transformation and aggregation in OtherStats and TableCard components — verify mapping correctness and performance with large datasets
    • Client-side tracking payload assembly and Beacon fallback in apps/web/app/s/[videoId]/Share.tsx — check session persistence and de-duplication logic
    • RPC bulk analytics fetch with dataloader and error per-item handling in packages/web-backend/src/Videos/index.ts and hook in apps/web/lib/Queries/Analytics.ts — verify concurrency and error isolation
    • Tinybird schema definitions and materialized view logic (pipes) — confirm SQL correctness, partition/sort key efficiency, and TTL settings
    • API route org/auth/range validation and fallback range handling in apps/web/app/api/dashboard/analytics/route.ts — ensure proper access control

Possibly related PRs

  • Cap#1171 — Expands and continues the analytics work from this PR, modifying useVideosAnalyticsQuery, AnalyticsRequest, and VideosGetAnalytics RPC surface to integrate Tinybird backend.
  • Cap#869 — Related by Effect-based RPC infrastructure and Tinybird integration in packages/web-backend, Videos service, and Effect runtime refactoring.
  • Cap#997 — Overlaps on cap deletion and list-rendering logic in Caps.tsx and FolderVideosSection.tsx touch points.

Suggested reviewers

  • Brendonovich

🐰 Charts and views now dance with Tinybird's grace,
Analytics bloom across the dashboard space,
Animated icons hop with motion's delight,
Sessions tracked true, from morning to night! ✨📊

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
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.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Implement Cap Analytics' clearly and concisely summarizes the primary change: introducing a comprehensive analytics feature to the Cap application. It accurately reflects the substantial scope of work across the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch analytics-project

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.

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: 22

Caution

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

⚠️ Outside diff range comments (4)
apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx (1)

56-74: Guard dynamic route params before building the title map

useParams() returns Readonly<Record<string, string | string[]>>, so params.id is a string | string[] | undefined. Interpolating that union into the template literal keys trips the type checker (the string[] branch isn’t allowed) and, on routes without an id, we end up registering /dashboard/folder/undefined and /dashboard/analytics/s/undefined. Build will fail once TS catches this.

Please type-narrow the param once and only append the dynamic keys when it’s present. For example:

- const params = useParams();
+ const params = useParams<{ id?: string }>();
+ const routeId = Array.isArray(params.id) ? params.id[0] : params.id;

- const titles: Record<string, string> = {
+ const titles: Record<string, string> = {
   "/dashboard/caps": "Caps",
   "/dashboard/folder": "Caps",
   "/dashboard/shared-caps": "Shared Caps",
   "/dashboard/caps/record": "Record a Cap",
   "/dashboard/settings/organization": "Organization Settings",
   "/dashboard/settings/account": "Account Settings",
   "/dashboard/spaces": "Spaces",
   "/dashboard/spaces/browse": "Browse Spaces",
-  "/dashboard/analytics": "Analytics",
-  [`/dashboard/folder/${params.id}`]: "Caps",
-  [`/dashboard/analytics/s/${params.id}`]: "Analytics: Cap video title",
- };
+  "/dashboard/analytics": "Analytics",
+ };
+
+ if (routeId) {
+  titles[`/dashboard/folder/${routeId}`] = "Caps";
+  titles[`/dashboard/analytics/s/${routeId}`] =
+    "Analytics: Cap video title";
+ }

This keeps the type checker happy and avoids the unintended .../undefined entries.

apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)

1-43: Fix import sorting as indicated by pipeline failure.

The pipeline reports that imports and exports are not sorted. Run pnpm format to fix this automatically.

As per coding guidelines.

apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx (1)

1-1: Fix formatting.

#!/bin/bash
pnpm format apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)

140-156: Fix Tinybird download blob retrieval

fetchResponse.arrayBuffer is a function; yielding it without calling returns the function itself, so the download flow blows up and never produces a blob. Invoke the method before yielding so you get the ArrayBuffer.

Apply this diff to fix it:

-					const blob = yield* fetchResponse.arrayBuffer;
+					const blob = yield* fetchResponse.arrayBuffer();
🧹 Nitpick comments (11)
apps/web/app/globals.css (1)

54-74: Remove duplicate .dashboard-grid CSS rule.

The .dashboard-grid class is defined twice identically (lines 54–63 and 65–74). Remove one of these duplicate definitions.

 .dashboard-grid {
 	display: grid;
 	grid-template-columns: auto 1fr 1fr;
 	grid-template-areas:
 		"sidebar main main"
 		"sidebar main main";
 	height: 100dvh;
 	width: 100vw;
 	min-height: 100dvh;
 }
 
-.dashboard-grid {
-	display: grid;
-	grid-template-columns: auto 1fr 1fr;
-	grid-template-areas:
-		"sidebar main main"
-		"sidebar main main";
-	height: 100dvh;
-	width: 100vw;
-	min-height: 100dvh;
-}
-
 .left-perspective {
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)

54-82: Consider removing the unused subNav property.

The subNav property is consistently set to an empty array and is never referenced in the component logic. This appears to be dead code.

Apply this diff if sub-navigation is not planned:

   const manageNavigation = [
     {
       name: "My Caps",
       href: `/dashboard/caps`,
       extraText: userCapsCount,
       icon: <CapIcon />,
-      subNav: [],
     },
     {
       name: "Analytics",
       href: `/dashboard/analytics`,
       ignoreParams: true,
       icon: <ChartLineIcon />,
-      subNav: [],
     },
     {
       name: "Record a Cap",
       href: `/dashboard/caps/record`,
       icon: <RecordIcon />,
-      subNav: [],
     },
     {
       name: "Organization Settings",
       href: `/dashboard/settings/organization`,
       ownerOnly: true,
       icon: <CogIcon />,
-      subNav: [],
     },
   ];
apps/web/app/api/analytics/route.ts (1)

28-31: Drop the ad-hoc console debugging

The new console.log calls will spam production logs with video identifiers and result payloads. Please remove them (or replace with structured logging behind the existing logging stack if truly needed).

apps/web/app/(org)/dashboard/analytics/components/VideoComponents.tsx (1)

129-158: Forward the id prop to the drop zone

VideoDroppable accepts an id, but it is never applied to the DOM element. Please wire it through so consumers can target the drop zone reliably.

-> ({ droppedValue, onRemove, isDragging, dragPosition, label }, ref) => {
+> ({ id, droppedValue, onRemove, isDragging, dragPosition, label }, ref) => {
     ...
-    <div
+    <div
+      id={id}
       ref={ref}
apps/web/app/s/[videoId]/Share.tsx (3)

38-63: Rename constant to reflect localStorage usage.

The constant SESSION_STORAGE_KEY suggests sessionStorage but the implementation uses localStorage (lines 44, 54). This naming inconsistency can confuse developers.

Consider renaming to ANALYTICS_SESSION_KEY or LOCAL_STORAGE_SESSION_KEY:

-const SESSION_STORAGE_KEY = "cap_tb_session_id";
+const ANALYTICS_SESSION_KEY = "cap_tb_session_id";

And update the references accordingly.


65-137: Inconsistent return type in trackVideoView.

The function returns a cleanup function only when using the fetch fallback (line 136), but returns undefined when using sendBeacon (line 115 implicit return). This inconsistency could confuse callers expecting a cleanup function.

Consider normalizing the return type:

  if (queued) {
-   return;
+   return () => {}; // no-op cleanup for sendBeacon path
  }

Alternatively, adjust the return type to explicitly allow void | (() => void).


278-291: Consider error handling in analytics tracking effect.

The useEffect doesn't handle potential errors from trackVideoView. While the tracking function has internal error handling, any synchronous errors (e.g., from ensureAnalyticsSessionId) would propagate and could break the component.

Wrap the tracking call in a try-catch to ensure component stability:

  useEffect(() => {
    if (viewerId && viewerId === data.owner.id) {
      return;
    }

+   try {
      const dispose = trackVideoView({
        videoId: data.id,
        orgId: data.orgId,
        ownerId: data.owner.id,
      });
      return () => {
        dispose?.();
      };
+   } catch (error) {
+     console.warn("Failed to initialize analytics tracking", error);
+   }
  }, [data.id, data.orgId, data.owner.id, viewerId]);
packages/web-backend/src/Tinybird/index.ts (2)

160-173: Hard-coded type conversion limits extensibility.

Line 168 special-cases the "views" key for numeric conversion, but this approach doesn't scale. If additional numeric columns are added (e.g., "clicks", "shares"), they won't be converted unless explicitly added.

Consider one of these approaches:

Option 1: Define known numeric columns

+ const NUMERIC_COLUMNS = new Set(["views", "clicks", "shares", "count"]);
  const parseTsvToObjects = <T>(text: string, aliases: string[]) => {
    // ... 
    for (let i = 0; i < values.length; i++) {
      const key = aliases[i] ?? `col_${i}`;
      const raw = values[i] ?? "";
-     obj[key] = key === "views" ? Number(raw) : raw;
+     obj[key] = NUMERIC_COLUMNS.has(key) ? Number(raw) : raw;
    }

Option 2: Attempt numeric conversion for numeric-looking values

- obj[key] = key === "views" ? Number(raw) : raw;
+ const num = Number(raw);
+ obj[key] = !Number.isNaN(num) && raw !== "" ? num : raw;

Note: TSV format lacks type metadata, so perfect inference is impossible without schema information.


175-206: Consider omitting optional fields instead of empty strings.

Lines 181-191 set optional fields to empty strings (e.g., session_id: row.session_id ?? "") rather than omitting them. This can:

  • Increase storage size unnecessarily
  • Make queries less efficient (empty strings vs. true nulls)
  • Complicate data analysis

If Tinybird's schema supports nullable fields, consider sending only defined values:

  const body = rows
    .map((row) =>
      JSON.stringify({
        ...row,
-       session_id: row.session_id ?? "",
-       user_id: row.user_id ?? "",
        // only include defined values
      }),
    )
    .join("\n");

Verify with Tinybird documentation whether nullable fields are supported in NDJSON ingestion.

apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx (2)

52-63: Non-null assertion could be unsafe.

Line 56 uses a non-null assertion (data[1]!) which could throw if data.length === 1. While line 55 checks data.length > 1, the assertion bypasses TypeScript's safety.

Use optional chaining instead:

  const bucketDuration =
    data.length > 1
-     ? new Date(data[1]!.bucket).getTime() - new Date(data[0]!.bucket).getTime()
+     ? new Date(data[1].bucket).getTime() - new Date(data[0].bucket).getTime()
      : 0;

The check on line 55 already ensures data[1] exists, so the non-null assertion is redundant and removes TypeScript's safety net.


148-155: Potential X-axis label overlap with large datasets.

Line 152 sets interval={0} which forces rendering all X-axis labels. With many data points (e.g., 24 hours of hourly data), labels could overlap and become unreadable.

Consider one of these approaches:

Option 1: Dynamic interval based on data length

+ const tickInterval = Math.max(0, Math.floor(chartData.length / 10));
  <XAxis
    dataKey="label"
    axisLine={false}
    tickMargin={10}
-   interval={0}
+   interval={tickInterval}
    height={40}
    tick={{ fontSize: 11 }}
  />

Option 2: Use angle for rotated labels

  <XAxis
    dataKey="label"
    axisLine={false}
    tickMargin={10}
    interval={0}
    height={40}
-   tick={{ fontSize: 11 }}
+   tick={{ fontSize: 11, angle: -45, textAnchor: 'end' }}
  />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fdec142 and 4838494.

⛔ Files ignored due to path filters (22)
  • apps/web/public/logos/browsers/aloha.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/bing.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/brave.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/chronium.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/duckduckgo.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/explorer.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/firefox.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/google-chrome.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/maxthron.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/opera-gx.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/opera.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/safari.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/tor.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/vivaldi.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/waterfox.svg is excluded by !**/*.svg
  • apps/web/public/logos/browsers/yandex.svg is excluded by !**/*.svg
  • apps/web/public/logos/os/fedora.svg is excluded by !**/*.svg
  • apps/web/public/logos/os/ios.svg is excluded by !**/*.svg
  • apps/web/public/logos/os/linux.svg is excluded by !**/*.svg
  • apps/web/public/logos/os/ubuntu.svg is excluded by !**/*.svg
  • apps/web/public/logos/os/windows.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (66)
  • .gitignore (1 hunks)
  • README.md (1 hunks)
  • apps/web/actions/videos/get-analytics.ts (1 hunks)
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/ChartLine.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/Clap.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/Reaction.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (2 hunks)
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (2 hunks)
  • apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/CompareFilters.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/FiltersList.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/Header.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/OtherStatBox.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/VideoComponents.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/VideoFilters.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/VideosPicker.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/data.ts (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/page.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/s/[id]/page.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/types.ts (1 hunks)
  • apps/web/app/(org)/dashboard/caps/Caps.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/caps/page.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx (2 hunks)
  • apps/web/app/(org)/dashboard/folder/[id]/page.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx (2 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx (2 hunks)
  • apps/web/app/(org)/dashboard/spaces/browse/page.tsx (1 hunks)
  • apps/web/app/api/analytics/route.ts (1 hunks)
  • apps/web/app/api/analytics/track/route.ts (1 hunks)
  • apps/web/app/api/dashboard/analytics/route.ts (1 hunks)
  • apps/web/app/globals.css (1 hunks)
  • apps/web/app/layout.tsx (0 hunks)
  • apps/web/app/s/[videoId]/Share.tsx (5 hunks)
  • apps/web/app/s/[videoId]/page.tsx (1 hunks)
  • apps/web/components/Tooltip.tsx (1 hunks)
  • apps/web/components/ui/chart.tsx (1 hunks)
  • apps/web/lib/Queries/Analytics.ts (1 hunks)
  • apps/web/lib/server.ts (2 hunks)
  • apps/web/package.json (5 hunks)
  • package.json (2 hunks)
  • packages/database/auth/auth-options.ts (0 hunks)
  • packages/env/server.ts (1 hunks)
  • packages/ui/src/components/Button.tsx (1 hunks)
  • packages/ui/src/components/Select.tsx (9 hunks)
  • packages/web-backend/src/Tinybird/index.ts (1 hunks)
  • packages/web-backend/src/Videos/VideosRpcs.ts (1 hunks)
  • packages/web-backend/src/Videos/index.ts (4 hunks)
  • packages/web-backend/src/index.ts (1 hunks)
  • scripts/analytics/check-analytics.js (1 hunks)
  • scripts/analytics/delete-all-data.js (1 hunks)
  • scripts/analytics/populate-test-data.js (1 hunks)
  • scripts/analytics/setup-analytics.js (1 hunks)
  • scripts/analytics/shared.js (1 hunks)
  • scripts/analytics/tinybird/datasources/analytics_events.datasource (1 hunks)
  • scripts/analytics/tinybird/datasources/analytics_pages_mv.datasource (1 hunks)
  • scripts/analytics/tinybird/datasources/analytics_sessions_mv.datasource (1 hunks)
  • scripts/analytics/tinybird/pipes/analytics_pages_mv_pipe.pipe (1 hunks)
  • scripts/analytics/tinybird/pipes/analytics_sessions_mv_pipe.pipe (1 hunks)
💤 Files with no reviewable changes (2)
  • packages/database/auth/auth-options.ts
  • apps/web/app/layout.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Files:

  • apps/web/app/(org)/dashboard/analytics/components/VideosPicker.tsx
  • apps/web/app/(org)/dashboard/analytics/page.tsx
  • apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx
  • packages/env/server.ts
  • packages/ui/src/components/Button.tsx
  • apps/web/components/Tooltip.tsx
  • apps/web/lib/Queries/Analytics.ts
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/Clap.tsx
  • apps/web/app/(org)/dashboard/analytics/components/VideoComponents.tsx
  • apps/web/app/api/analytics/route.ts
  • apps/web/app/(org)/dashboard/analytics/data.ts
  • packages/web-backend/src/index.ts
  • apps/web/app/(org)/dashboard/folder/[id]/page.tsx
  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
  • packages/web-backend/src/Videos/VideosRpcs.ts
  • apps/web/app/(org)/dashboard/analytics/components/VideoFilters.tsx
  • packages/web-backend/src/Videos/index.ts
  • packages/web-backend/src/Tinybird/index.ts
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/Reaction.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/(org)/dashboard/analytics/components/Header.tsx
  • apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx
  • apps/web/app/(org)/dashboard/analytics/s/[id]/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx
  • apps/web/lib/server.ts
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
  • apps/web/app/(org)/dashboard/spaces/browse/page.tsx
  • apps/web/app/api/dashboard/analytics/route.ts
  • apps/web/app/(org)/dashboard/analytics/types.ts
  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/FiltersList.tsx
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/ChartLine.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts
  • apps/web/app/(org)/dashboard/analytics/components/CompareFilters.tsx
  • apps/web/app/(org)/dashboard/analytics/components/OtherStatBox.tsx
  • apps/web/components/ui/chart.tsx
  • apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx
  • apps/web/actions/videos/get-analytics.ts
  • packages/ui/src/components/Select.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/web/app/(org)/dashboard/analytics/components/VideosPicker.tsx
  • apps/web/app/(org)/dashboard/analytics/page.tsx
  • apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx
  • packages/env/server.ts
  • packages/ui/src/components/Button.tsx
  • apps/web/components/Tooltip.tsx
  • apps/web/lib/Queries/Analytics.ts
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/Clap.tsx
  • apps/web/app/(org)/dashboard/analytics/components/VideoComponents.tsx
  • apps/web/app/api/analytics/route.ts
  • apps/web/app/(org)/dashboard/analytics/data.ts
  • packages/web-backend/src/index.ts
  • apps/web/app/(org)/dashboard/folder/[id]/page.tsx
  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
  • packages/web-backend/src/Videos/VideosRpcs.ts
  • apps/web/app/(org)/dashboard/analytics/components/VideoFilters.tsx
  • packages/web-backend/src/Videos/index.ts
  • packages/web-backend/src/Tinybird/index.ts
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/Reaction.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/(org)/dashboard/analytics/components/Header.tsx
  • apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx
  • scripts/analytics/delete-all-data.js
  • scripts/analytics/shared.js
  • apps/web/app/(org)/dashboard/analytics/s/[id]/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx
  • apps/web/lib/server.ts
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
  • apps/web/app/(org)/dashboard/spaces/browse/page.tsx
  • scripts/analytics/populate-test-data.js
  • apps/web/app/api/dashboard/analytics/route.ts
  • apps/web/app/(org)/dashboard/analytics/types.ts
  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/FiltersList.tsx
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/ChartLine.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • scripts/analytics/setup-analytics.js
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts
  • apps/web/app/(org)/dashboard/analytics/components/CompareFilters.tsx
  • apps/web/app/(org)/dashboard/analytics/components/OtherStatBox.tsx
  • apps/web/components/ui/chart.tsx
  • apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx
  • scripts/analytics/check-analytics.js
  • apps/web/actions/videos/get-analytics.ts
  • packages/ui/src/components/Select.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

On the client, always use useEffectQuery or useEffectMutation from @/lib/EffectRuntime; never call EffectRuntime.run* directly in components.

Files:

  • apps/web/app/(org)/dashboard/analytics/components/VideosPicker.tsx
  • apps/web/app/(org)/dashboard/analytics/page.tsx
  • apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx
  • apps/web/components/Tooltip.tsx
  • apps/web/lib/Queries/Analytics.ts
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/Clap.tsx
  • apps/web/app/(org)/dashboard/analytics/components/VideoComponents.tsx
  • apps/web/app/api/analytics/route.ts
  • apps/web/app/(org)/dashboard/analytics/data.ts
  • apps/web/app/(org)/dashboard/folder/[id]/page.tsx
  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
  • apps/web/app/(org)/dashboard/analytics/components/VideoFilters.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/Reaction.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/(org)/dashboard/analytics/components/Header.tsx
  • apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx
  • apps/web/app/(org)/dashboard/analytics/s/[id]/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx
  • apps/web/lib/server.ts
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
  • apps/web/app/(org)/dashboard/spaces/browse/page.tsx
  • apps/web/app/api/dashboard/analytics/route.ts
  • apps/web/app/(org)/dashboard/analytics/types.ts
  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/FiltersList.tsx
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/ChartLine.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts
  • apps/web/app/(org)/dashboard/analytics/components/CompareFilters.tsx
  • apps/web/app/(org)/dashboard/analytics/components/OtherStatBox.tsx
  • apps/web/components/ui/chart.tsx
  • apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx
  • apps/web/actions/videos/get-analytics.ts
packages/ui/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Component files in packages/ui should use PascalCase naming if they define React/Solid components.

Files:

  • packages/ui/src/components/Button.tsx
  • packages/ui/src/components/Select.tsx
🧠 Learnings (4)
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/**/*.{ts,tsx,js,jsx} : On the client, always use `useEffectQuery` or `useEffectMutation` from `@/lib/EffectRuntime`; never call `EffectRuntime.run*` directly in components.

Applied to files:

  • apps/web/app/s/[videoId]/Share.tsx
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : API routes in Next.js (`apps/web/app/api/*`) must use `effect/platform`'s `HttpApi` builder and follow the existing class/group/endpoint pattern; avoid ad-hoc handlers.

Applied to files:

  • apps/web/app/api/analytics/track/route.ts
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : Convert effectful APIs to Next.js handlers using `apiToHandler(ApiLive)` and export only the returned `handler`; avoid calling `runPromise` directly in route files.

Applied to files:

  • apps/web/app/api/analytics/track/route.ts
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to packages/ui/**/*.{ts,tsx,js,jsx} : Component files in `packages/ui` should use PascalCase naming if they define React/Solid components.

Applied to files:

  • packages/ui/src/components/Select.tsx
🧬 Code graph analysis (31)
apps/web/app/(org)/dashboard/analytics/components/VideosPicker.tsx (1)
apps/web/app/(org)/dashboard/analytics/components/VideoComponents.tsx (1)
  • VideoDroppable (101-159)
apps/web/app/(org)/dashboard/analytics/page.tsx (1)
apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx (1)
  • AnalyticsDashboard (23-123)
apps/web/lib/Queries/Analytics.ts (2)
apps/web/lib/Requests/AnalyticsRequest.ts (1)
  • AnalyticsRequest (8-12)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectQuery (22-22)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Clap.tsx (1)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (1)
  • ClapIcon (28-28)
apps/web/app/api/analytics/route.ts (2)
apps/web/app/api/dashboard/analytics/route.ts (1)
  • GET (11-41)
apps/web/actions/videos/get-analytics.ts (1)
  • getVideoAnalytics (37-99)
apps/web/app/(org)/dashboard/analytics/data.ts (6)
packages/database/schema.ts (3)
  • videos (291-363)
  • spaceVideos (665-688)
  • comments (394-420)
apps/web/app/(org)/dashboard/analytics/types.ts (3)
  • AnalyticsRange (1-1)
  • OrgAnalyticsResponse (10-33)
  • BreakdownRow (3-8)
apps/web/actions/spaces/get-space-videos.ts (1)
  • getSpaceVideoIds (9-56)
packages/database/index.ts (1)
  • db (18-25)
apps/web/lib/server.ts (1)
  • runPromise (141-153)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-274)
apps/web/app/(org)/dashboard/caps/page.tsx (1)
packages/env/server.ts (1)
  • serverEnv (136-140)
apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx (5)
apps/web/app/(org)/dashboard/analytics/types.ts (2)
  • AnalyticsRange (1-1)
  • OrgAnalyticsResponse (10-33)
apps/web/app/(org)/dashboard/Contexts.tsx (1)
  • useDashboardContext (50-50)
packages/web-domain/src/Organisation.ts (1)
  • Organisation (21-24)
apps/web/app/(org)/dashboard/analytics/components/Header.tsx (1)
  • Header (90-429)
apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx (1)
  • OtherStats (122-214)
apps/web/app/(org)/dashboard/caps/Caps.tsx (1)
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (1)
  • FolderDataType (18-25)
apps/web/app/(org)/dashboard/analytics/components/VideoFilters.tsx (1)
apps/web/app/(org)/dashboard/analytics/components/VideoComponents.tsx (1)
  • DraggableVideoItem (19-90)
packages/web-backend/src/Videos/index.ts (3)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-274)
packages/web-backend/src/Videos/VideosPolicy.ts (1)
  • VideosPolicy (9-96)
packages/web-backend/src/Videos/VideosRepo.ts (1)
  • VideosRepo (15-106)
packages/web-backend/src/Tinybird/index.ts (3)
packages/web-backend/src/index.ts (1)
  • Tinybird (18-18)
packages/env/server.ts (1)
  • serverEnv (136-140)
scripts/analytics/shared.js (2)
  • request (134-165)
  • raw (88-88)
apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx (3)
apps/web/app/(org)/dashboard/analytics/types.ts (1)
  • BreakdownRow (3-8)
apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx (6)
  • DeviceRowData (100-107)
  • CountryRowData (64-71)
  • CityRowData (73-80)
  • BrowserRowData (82-89)
  • OSRowData (91-98)
  • CapRowData (109-116)
apps/web/app/(org)/dashboard/analytics/components/OtherStatBox.tsx (1)
  • OtherStatBox (12-32)
apps/web/app/(org)/dashboard/analytics/components/Header.tsx (4)
apps/web/app/(org)/dashboard/analytics/types.ts (1)
  • AnalyticsRange (1-1)
packages/web-domain/src/Organisation.ts (1)
  • Organisation (21-24)
apps/web/app/Layout/AuthContext.tsx (1)
  • useCurrentUser (40-42)
apps/web/components/SignedImageUrl.tsx (1)
  • SignedImageUrl (11-25)
apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx (2)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Cap.tsx (1)
  • CapIconHandle (8-11)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (4)
  • ClapIcon (28-28)
  • ChatIcon (29-29)
  • ReactionIcon (30-30)
  • CapIcon (18-18)
scripts/analytics/delete-all-data.js (3)
scripts/analytics/check-analytics.js (2)
  • client (154-154)
  • datasource (46-46)
scripts/analytics/setup-analytics.js (1)
  • auth (80-80)
scripts/analytics/shared.js (1)
  • auth (132-132)
scripts/analytics/shared.js (2)
scripts/analytics/delete-all-data.js (2)
  • auth (68-68)
  • payload (8-8)
scripts/analytics/setup-analytics.js (1)
  • auth (80-80)
apps/web/app/(org)/dashboard/analytics/s/[id]/page.tsx (3)
apps/web/app/(org)/dashboard/analytics/page.tsx (1)
  • AnalyticsPage (3-5)
apps/web/app/(org)/dashboard/analytics/components/Header.tsx (1)
  • Header (90-429)
apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx (2)
  • OtherStats (122-214)
  • OtherStatsData (20-27)
apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx (1)
apps/web/components/ui/chart.tsx (4)
  • ChartConfig (10-18)
  • ChartContainer (354-354)
  • ChartTooltip (355-355)
  • ChartTooltipContent (356-356)
apps/web/lib/server.ts (2)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-274)
packages/web-backend/src/index.ts (1)
  • Tinybird (18-18)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (2)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx (1)
  • CapCardButton (18-46)
packages/ui/src/components/Dropdown.tsx (4)
  • DropdownMenu (191-191)
  • DropdownMenuTrigger (205-205)
  • DropdownMenuContent (193-193)
  • DropdownMenuItem (195-195)
scripts/analytics/populate-test-data.js (1)
scripts/analytics/migrate-dub-to-tinybird.js (17)
  • os (5188-5188)
  • search (4736-4736)
  • url (4428-4428)
  • url (4737-4737)
  • response (4433-4435)
  • response (4738-4746)
  • text (4439-4439)
  • text (4747-4747)
  • message (4450-4450)
  • message (4767-4767)
  • videoIds (5345-5345)
  • i (4116-4116)
  • i (4180-4180)
  • i (4721-4721)
  • i (5309-5309)
  • totalWritten (5410-5410)
  • chunks (5308-5308)
apps/web/app/api/dashboard/analytics/route.ts (2)
apps/web/app/(org)/dashboard/analytics/types.ts (1)
  • AnalyticsRange (1-1)
apps/web/app/(org)/dashboard/analytics/data.ts (1)
  • getOrgAnalyticsData (119-294)
apps/web/app/api/analytics/track/route.ts (3)
apps/web/lib/server.ts (1)
  • runPromise (141-153)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-274)
packages/web-backend/src/Auth.ts (1)
  • provideOptionalAuth (102-114)
apps/web/app/(org)/dashboard/analytics/components/FiltersList.tsx (1)
apps/web/app/(org)/dashboard/analytics/components/CompareFilters.tsx (2)
  • FilterValue (33-33)
  • CompareDataFilterItem (44-119)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/ChartLine.tsx (1)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (1)
  • ChartLineIcon (27-27)
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (4)
apps/web/app/(org)/dashboard/Contexts.tsx (1)
  • useDashboardContext (50-50)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (4)
  • CapIcon (18-18)
  • ChartLineIcon (27-27)
  • RecordIcon (31-31)
  • CogIcon (20-20)
apps/web/components/SignedImageUrl.tsx (1)
  • SignedImageUrl (11-25)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Cog.tsx (1)
  • CogIconHandle (8-11)
scripts/analytics/setup-analytics.js (2)
scripts/analytics/shared.js (2)
  • PROJECT_ROOT (9-9)
  • auth (132-132)
scripts/analytics/delete-all-data.js (1)
  • auth (68-68)
apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx (2)
packages/ui/src/components/Table.tsx (6)
  • Table (117-117)
  • TableHeader (123-123)
  • TableRow (124-124)
  • TableHead (122-122)
  • TableBody (118-118)
  • TableCell (120-120)
packages/ui/src/components/icons/LogoBadge.tsx (1)
  • LogoBadge (1-28)
scripts/analytics/check-analytics.js (1)
scripts/analytics/shared.js (4)
  • normalizeWhitespace (74-74)
  • buildSchemaLines (76-76)
  • TABLE_DEFINITIONS (16-67)
  • PIPE_DEFINITIONS (69-72)
apps/web/actions/videos/get-analytics.ts (5)
packages/database/index.ts (1)
  • db (18-25)
packages/database/schema.ts (1)
  • videos (291-363)
packages/web-domain/src/Video.ts (1)
  • Video (16-59)
apps/web/lib/server.ts (1)
  • runPromise (141-153)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-274)
🪛 ast-grep (0.39.9)
apps/web/components/ui/chart.tsx

[warning] 80-80: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 GitHub Actions: CI
apps/web/app/(org)/dashboard/analytics/data.ts

[error] 1-1: File content differs from formatting output

packages/web-backend/src/index.ts

[error] 1-1: File content differs from formatting output


[error] 1-1: File content differs from formatting output

apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx

[error] 13-14: File content differs from formatting output

packages/web-backend/src/Videos/VideosRpcs.ts

[error] 111-111: File content differs from formatting output

packages/web-backend/src/Videos/index.ts

[error] 17-17: File content differs from formatting output

packages/web-backend/src/Tinybird/index.ts

[error] 1-1: File content differs from formatting output

apps/web/app/(org)/dashboard/analytics/components/Header.tsx

[error] 13-13: File content differs from formatting output

scripts/analytics/delete-all-data.js

[error] 1-1: File content differs from formatting output

apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx

[error] 54-54: File content differs from formatting output

apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx

[error] 1-1: File content differs from formatting output


[error] 1-1: File content differs from formatting output

scripts/analytics/populate-test-data.js

[error] 6-6: File content differs from formatting output

apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx

[error] 1-1: The imports and exports are not sorted.

scripts/analytics/setup-analytics.js

[error] 17-17: File content differs from formatting output

scripts/analytics/check-analytics.js

[error] 3-3: File content differs from formatting output

apps/web/actions/videos/get-analytics.ts

[error] 1-1: The imports and exports are not sorted.

🪛 GitHub Check: CodeQL
scripts/analytics/populate-test-data.js

[failure] 56-56: Insecure randomness
This uses a cryptographically insecure random number generated at Math.random() in a security context.

🪛 GitHub Check: Typecheck
apps/web/app/(org)/dashboard/analytics/s/[id]/page.tsx

[failure] 46-46:
Type 'string' is not assignable to type 'number'.


[failure] 43-43:
Type 'string' is not assignable to type 'number'.


[failure] 38-38:
Type 'string' is not assignable to type 'number'.


[failure] 35-35:
Type 'string' is not assignable to type 'number'.


[failure] 30-30:
Type 'string' is not assignable to type 'number'.


[failure] 27-27:
Type 'string' is not assignable to type 'number'.


[failure] 13-13:
Type 'string' is not assignable to type 'number'.


[failure] 11-11:
Type 'string' is not assignable to type 'number'.


[failure] 8-8:
Type '{}' is missing the following properties from type 'HeaderProps': options, value, onChange

🪛 LanguageTool
README.md

[typographical] ~66-~66: Consider adding a comma here.
Context: ... in your environment. Once the token is present you can: - Provision the required data...

(IF_THAT_S_NOT_POSSIBLE_COMMA)


[grammar] ~71-~71: The word ‘deploy’ is a verb. Did you mean the noun “deployment” (= release, placement)?
Context: ...able with the destructive nature of the deploy step before running analytics:setup. ...

(PREPOSITION_VERB)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (39)
apps/web/app/(org)/dashboard/spaces/browse/page.tsx (1)

3-3: ✓ Avatar import removal looks good.

The Avatar import has been removed from the "@cap/ui" import statement, and there are no usages of Avatar in the component. The SignedImageUrl component is correctly being used for space icon rendering instead. This change is clean and isolated.

apps/web/app/globals.css (1)

339-346: Verify visual impact of removing font-normal.

Removing the font-normal utility from this selector group means these elements (a, p, span, input, label, button) will now inherit font-weight from their parent instead of forcing normal weight. This could cause visual inconsistencies if parent elements have font-weight values other than normal applied.

Confirm that this change has been tested and doesn't cause unintended visual regressions across the UI.

packages/ui/src/components/Button.tsx (1)

24-24: LGTM! White variant styling updated appropriately.

The updated styling provides a well-defined hover progression (gray-3 → gray-6) and maintains consistency with the structure of other button variants. The change aligns with the broader UI theming work for the analytics feature.

README.md (1)

64-71: Appreciate the precise Tinybird setup notes. Clear callouts about the destructive deploy make it easy for operators to assess the risk before running the scripts.

.gitignore (1)

38-41: Good call adding Tinybird credential ignores. This keeps the copied .tinyb admin tokens out of version control.

apps/web/components/Tooltip.tsx (1)

15-29: Forwarding delayDuration looks solid. Optional prop preserves existing behavior while enabling finer control when needed.

scripts/analytics/tinybird/pipes/analytics_pages_mv_pipe.pipe (1)

1-18: Materialized view query looks correct. Aggregating uniqState(session_id) across tenant/path/date will feed the MV as expected.

apps/web/app/s/[videoId]/page.tsx (1)

743-752: Thanks for threading viewerId through. This keeps the page ready for the new analytics hooks without altering existing behavior.

scripts/analytics/delete-all-data.js (1)

1-135: Fix formatting to unblock CI. The pipeline is failing with “File content differs from formatting output.” Please run pnpm format (Biome) so the script matches the expected formatting.

apps/web/actions/videos/get-analytics.ts (1)

3-12: Fix import ordering to satisfy Biome & unblock CI

CI is red with “The imports and exports are not sorted.” Please run the project formatter (pnpm format / Biome) so the import block is reordered as expected before rebasing.

apps/web/app/(org)/dashboard/analytics/page.tsx (1)

1-5: LGTM!

Clean implementation of the analytics page entry point following React and Next.js conventions.

packages/env/server.ts (1)

95-98: LGTM!

Properly configured optional Tinybird environment variables with clear documentation.

apps/web/lib/server.ts (1)

20-20: LGTM!

Correctly integrates Tinybird service into the Effect runtime dependencies layer.

Also applies to: 117-117

apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx (1)

106-108: LGTM!

Correctly gates analytics on both Tinybird environment variables, aligning with the migration from Dub to Tinybird-based analytics.

apps/web/package.json (1)

29-30: LGTM!

Appropriate dependency additions for the analytics dashboard functionality, including charting (recharts), internationalization (country-flag-icons), drag-and-drop (@dnd-kit), and user agent parsing (ua-parser-js).

Also applies to: 87-87, 125-125, 134-134, 150-150

apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx (1)

19-19: LGTM!

Clean refactor renaming dubApiKeyEnabled to analyticsEnabled, aligning with the broader migration from Dub-based analytics to Tinybird integration.

Also applies to: 24-24, 112-112

apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx (1)

35-82: LGTM!

Making analytics metrics clickable is a good UX improvement. The consistent routing to /dashboard/analytics?capId=${capId} enables users to drill down into detailed analytics for each cap.

apps/web/app/(org)/dashboard/caps/page.tsx (1)

269-271: LGTM!

The migration from dubApiKeyEnabled to analyticsEnabled with Tinybird credentials correctly gates the analytics feature based on the new Tinybird integration.

scripts/analytics/tinybird/datasources/analytics_sessions_mv.datasource (1)

1-16: LGTM!

The session-level analytics datasource is well-configured with appropriate partitioning by month, a composite sorting key optimized for tenant-scoped queries, and a 90-day TTL for data retention. The use of LowCardinality for high-cardinality string fields is a good optimization.

apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx (2)

56-61: LGTM!

The prop rename from dubApiKeyEnabled to analyticsEnabled is consistent with the Tinybird migration across the codebase.


99-102: LGTM!

The analytics query correctly uses the renamed analyticsEnabled prop.

apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx (2)

261-263: LGTM!

Consistent migration to Tinybird-based analytics gating for the space variant.


367-369: LGTM!

Consistent migration to Tinybird-based analytics gating for the organization variant.

scripts/analytics/tinybird/datasources/analytics_events.datasource (2)

4-24: LGTM!

The schema is comprehensive with appropriate field types, LowCardinality optimizations for categorical data, and sensible defaults. The MergeTree configuration with monthly partitioning and 90-day TTL is well-designed.


26-27: The FORWARD_QUERY is valid and complete—no issues to address.

Tinybird's FORWARD_QUERY specification requires only a SELECT column-selection part; FROM and WHERE clauses are not supported. The query in the datasource file follows this syntax correctly. The review comment incorrectly assumes a FROM clause is needed; it is not.

Likely an incorrect or invalid review comment.

scripts/analytics/populate-test-data.js (4)

45-56: Cryptographically insecure random is acceptable for test data.

The static analysis tool flags Math.random() usage in session ID generation as cryptographically insecure. For test data generation, this is acceptable since we're not generating security-sensitive values. However, ensure this script is never used to generate production data.


114-116: Verify SSL configuration for database connection.

The rejectUnauthorized: false setting bypasses SSL certificate verification, which is insecure. This may be necessary for development environments, but ensure this script is documented as dev-only and never used against production databases.


171-227: LGTM!

The main function orchestrates test data generation well with proper error handling, progress logging, and chunked ingestion. The distribution algorithm ensures realistic view counts across videos.


6-6: Fix formatting.

#!/bin/bash
pnpm format scripts/analytics/populate-test-data.js
⛔ Skipped due to learnings
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to **/*.{ts,tsx} : Use Biome for formatting and linting TypeScript/JavaScript files by running `pnpm format`.
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Always run `pnpm db:generate` followed by `pnpm db:push` before relying on new schema changes.
packages/web-backend/src/Videos/VideosRpcs.ts (1)

111-125: No actionable issues found; formatting is correct and error handling is valid.

The code at lines 111-125 uses proper 2-space indentation and follows the established pattern. While the error handling differs from VideosGetThumbnails, this is not a defect:

  • getAnalyticsBulkInternal returns Array<Exit.Exit<{ count: number }, unknown>>, meaning per-item errors are already wrapped as Exit objects at the backend
  • The catchTag handlers catch batch-level errors (database unreachable), which is appropriate
  • Per-item failures remain accessible via the Exit-wrapped results, enabling partial success handling at the client layer (similar to thumbnails)

Both RPC implementations use valid patterns—VideosGetThumbnails wraps per-call; VideosGetAnalytics wraps per-result in the backend function.

Likely an incorrect or invalid review comment.

apps/web/app/(org)/dashboard/caps/Caps.tsx (1)

58-84: LGTM! Clean prop rename with proper loading state handling.

The refactoring from dubApiKeyEnabled to analyticsEnabled is well-executed. The derived isLoadingAnalytics state correctly gates loading on both analytics being enabled and the query loading, preventing unnecessary loading indicators when analytics is disabled.

apps/web/app/s/[videoId]/Share.tsx (2)

139-155: LGTM! Proper viewer tracking with self-exclusion.

The addition of the viewerId prop and its use to skip self-tracking (line 279) is well-implemented. The flexible type string | null | undefined accommodates various authentication states.

Also applies to: 233-241


41-63: Session ID fallback to "anonymous" impacts data quality.

When localStorage operations fail (line 60), the code falls back to "anonymous" as the session ID. While this prevents tracking errors from breaking functionality, it may reduce analytics accuracy if many users hit this error path, as all failed sessions will be conflated.

Consider adding telemetry to monitor how often this fallback occurs:

  } catch (error) {
    console.warn("Failed to persist analytics session id", error);
+   // Consider: report this to your error tracking service
    return "anonymous";
  }
scripts/analytics/setup-analytics.js (2)

76-88: LGTM! Well-structured setup script with proper error handling.

The main function orchestrates the setup flow logically: ensure CLI → ensure login → resolve auth → sync credentials → deploy. Error handling via try-catch with clear error messages is appropriate.


1-90: Run formatter to fix formatting issues.

The pipeline indicates a formatting error on line 17. Please run the formatter to fix this issue.

#!/bin/bash
# Run biome formatter on the file
pnpm format
⛔ Skipped due to learnings
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to **/*.{ts,tsx} : Use Biome for formatting and linting TypeScript/JavaScript files by running `pnpm format`.
packages/web-backend/src/Tinybird/index.ts (2)

53-115: LGTM! Robust request helper with comprehensive error handling.

The request helper properly handles:

  • Early return when disabled (line 54)
  • Non-OK responses with detailed logging (lines 71-79)
  • JSON parsing failures with response preview (lines 87-98)
  • Multiple response formats via normalization (lines 101-105)

1-3: Run formatter to fix formatting issues.

The pipeline indicates a formatting error on line 1. Please run the formatter as per the coding guidelines.

#!/bin/bash
# Run biome formatter on the file
pnpm format
⛔ Skipped due to learnings
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to **/*.{ts,tsx} : Use Biome for formatting and linting TypeScript/JavaScript files by running `pnpm format`.
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : On the server, always run effects through `EffectRuntime.runPromise` after `provideOptionalAuth` to ensure cookies and per-request context are attached.
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/**/*.{ts,tsx,js,jsx} : On the client, always use `useEffectQuery` or `useEffectMutation` from `@/lib/EffectRuntime`; never call `EffectRuntime.run*` directly in components.
apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx (1)

65-106: LGTM! Well-designed Y-axis scaling logic.

The Y-axis calculation intelligently:

  • Handles edge cases (no data, no metrics, max = 0)
  • Rounds to "nice" values (1, 2, 5, 10 multiples) for readability
  • Generates evenly spaced ticks

The magnitude-based rounding ensures axis labels are human-friendly.

apps/web/app/(org)/dashboard/analytics/components/Header.tsx (1)

1-30: Run the formatter to unblock CI

CI is failing with “File content differs from formatting output.” Please run pnpm format (Biome) on this file so the pipeline passes.

Comment on lines 156 to 168
return (
<div
ref={ref}
className={clsx(
"h-6 w-[100px] transition-all border rounded-full file:flex items-center",
isDragging && !isOver && !droppedValue
? "bg-gray-5 border-gray-11"
: isOver && isDragging
? "bg-transparent border-blue-500"
: !droppedValue
? "border-dashed border-gray-7"
: "border-transparent",
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix droppable layout and forward the id prop.

The droppable wrapper drops the incoming id, so callers (and tests) can’t actually address the zone even though the prop exists. On top of that, file:flex is a Tailwind* variant that never applies here, so the container isn’t flexed and the alignment/hit-box styling regresses.

Apply this diff:

-export const CompareDataDroppable = React.forwardRef<
-	HTMLDivElement,
-	CompareDataDroppableProps
->(({ droppedValue, onRemove, isDragging, dragPosition }, ref) => {
+export const CompareDataDroppable = React.forwardRef<
+	HTMLDivElement,
+	CompareDataDroppableProps
+>(({ id, droppedValue, onRemove, isDragging, dragPosition }, ref) => {
@@
-		<div
-			ref={ref}
-			className={clsx(
-				"h-6 w-[100px] transition-all border rounded-full file:flex items-center",
+		<div
+			ref={ref}
+			id={id}
+			className={clsx(
+				"h-6 w-[100px] transition-all border rounded-full flex items-center",

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/analytics/components/CompareFilters.tsx around
lines 156 to 168, the droppable wrapper is dropping the incoming id prop and
uses the incorrect Tailwind variant "file:flex" so the container isn't flexed;
fix by forwarding the id prop to the returned div (e.g., <div id={id} ...>) so
callers/tests can target the zone, and replace "file:flex" with "flex" in the
className so the container is actually a flexbox and alignment/hit-box styles
work as intended.

Comment on lines 303 to 345
<SelectPrimitive.Viewport className="p-1">
<SelectPrimitive.Item
key={activeOrganization.organization.id}
value={activeOrganization.organization.id}
className="relative flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none transition-colors font-semibold text-gray-12 data-[state=checked]:bg-gray-3 data-[highlighted]:bg-gray-3 data-[highlighted]:text-gray-12"
>
<SignedImageUrl
image={
user?.imageUrl || activeOrganization.organization.iconUrl
}
name={user?.name || activeOrganization.organization.name}
className="size-5 flex-shrink-0"
/>
<SelectPrimitive.ItemText>
{user?.name ? `${user.name}'s Caps` : "My Caps"}
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
{filteredSpaces && filteredSpaces.length > 0 && (
<>
<div className="px-3 py-2 text-xs font-semibold text-gray-9 uppercase tracking-wider">
Spaces
</div>
{filteredSpaces.map((space) => {
return (
<SelectPrimitive.Item
key={space.id}
value={`space:${space.id}`}
className="relative flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none transition-colors text-gray-12 data-[state=checked]:bg-gray-3 data-[highlighted]:bg-gray-3 data-[highlighted]:text-gray-12"
>
<SignedImageUrl
image={space.iconUrl}
name={space.name}
className="size-5 flex-shrink-0"
/>
<SelectPrimitive.ItemText>
{space.name}
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
})}
</>
)}
</SelectPrimitive.Viewport>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restore organization switching options

The organization selector now renders only the active organization plus its spaces; the rest of organizations is never surfaced, so users can no longer switch to other orgs. That’s a regression from the previous behavior. Please restore the list of organizations in this menu (e.g., iterate organizations and render each one) before shipping.

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/analytics/components/Header.tsx around lines 303
to 345, the dropdown only lists the active organization and its spaces so users
cannot switch to other organizations; iterate the organizations array and render
a SelectPrimitive.Item for each organization (skipping or de-duplicating the
active org if desired) before rendering the Spaces section: for each org use
key={org.id} value={org.id}, render SignedImageUrl with org.iconUrl and
org.name, and SelectPrimitive.ItemText with org.name; optionally add a small
"Organizations" header above the list to match existing UI patterns.

Comment on lines 172 to 277
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
{icon && React.cloneElement(icon as React.ReactElement<{ className: string }>, { className: cx("size-3", "text-gray-9") })}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Apply the same safe icon handling here.

The same two issues occur for item icons: cloning a generic React.ReactNode will crash for valid inputs, and we drop any caller-supplied className. Mirror the guarded merge used above.

-      {icon && React.cloneElement(icon as React.ReactElement<{ className: string }>, { className: cx("size-3", "text-gray-9") })}
+      {React.isValidElement(icon)
+        ? React.cloneElement(icon, {
+            className: cx("size-3", "text-gray-9", icon.props.className),
+          })
+        : null}
📝 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
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
{icon && React.cloneElement(icon as React.ReactElement<{ className: string }>, { className: cx("size-3", "text-gray-9") })}
</div>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
{React.isValidElement(icon)
? React.cloneElement(icon, {
className: cx("size-3", "text-gray-9", icon.props.className),
})
: null}
</div>
🤖 Prompt for AI Agents
In packages/ui/src/components/Select.tsx around lines 275 to 277, the item icon
is being cloned unconditionally which can crash for non-element ReactNodes and
it overwrites any caller-supplied className; change this to mirror the guarded
pattern used elsewhere: check React.isValidElement(icon) before cloning, merge
the existing icon.props.className with "size-3" and "text-gray-9" via cx and
pass through all other props when cloning, and if icon is not a valid React
element simply render it as-is; ensure types reflect React.ReactNode and no
className from the caller is lost.

export { Videos } from "./Videos/index.ts";
export { VideosPolicy } from "./Videos/VideosPolicy.ts";
export { VideosRepo } from "./Videos/VideosRepo.ts";
export { Tinybird } from "./Tinybird/index.ts";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Run formatter to fix pipeline failure.

The pipeline indicates this file needs formatting.

Run the following command to fix the formatting:

#!/bin/bash
pnpm format
🤖 Prompt for AI Agents
In packages/web-backend/src/index.ts around line 18, the file is improperly
formatted causing the CI pipeline to fail; run the repository formatter to fix
whitespace/formatting issues by executing the project's format command (pnpm
format) locally, then stage and commit the resulting changes so the pipeline can
pass.

Comment on lines 28 to 51
export class Tinybird extends Effect.Service<Tinybird>()("Tinybird", {
effect: Effect.gen(function* () {
const env = serverEnv();
const token = env.TINYBIRD_TOKEN;
const host = env.TINYBIRD_HOST;

if (!host) {
yield* Effect.die(
new Error("TINYBIRD_HOST must be set"),
);
}

yield* Effect.logDebug("Initializing Tinybird service", {
hasToken: Boolean(token),
host,
});

const enabled = Boolean(token);

if (!enabled) {
yield* Effect.logWarning(
"Tinybird is disabled: TINYBIRD_TOKEN is not set",
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent environment variable validation.

The service dies if TINYBIRD_HOST is missing (line 35) but only logs a warning if TINYBIRD_TOKEN is missing (lines 48-50). This creates an inconsistency: why require host if the service can operate in a disabled mode without a token?

Consider one of the following approaches:

Option 1: Make both optional for disabled mode

  if (!host) {
-   yield* Effect.die(
-     new Error("TINYBIRD_HOST must be set"),
-   );
+   yield* Effect.logWarning(
+     "Tinybird is disabled: TINYBIRD_HOST is not set",
+   );
  }

- const enabled = Boolean(token);
+ const enabled = Boolean(token && host);

Option 2: Require both or neither

- if (!host) {
+ if (!host || !token) {
    yield* Effect.die(
-     new Error("TINYBIRD_HOST must be set"),
+     new Error("Both TINYBIRD_HOST and TINYBIRD_TOKEN must be set"),
    );
  }
🤖 Prompt for AI Agents
In packages/web-backend/src/Tinybird/index.ts around lines 28 to 51, the code
currently dies when TINYBIRD_HOST is missing but only logs a warning when
TINYBIRD_TOKEN is missing, causing inconsistent validation; pick one approach
and make the validation consistent: either (Option 1) treat both as optional for
a disabled mode—only log a warning and set enabled = false when token is absent
and do not die for missing host, or (Option 2) require both or neither—if either
token or host is provided, validate that both exist and call Effect.die with a
clear Error when one is missing; update the logging to reflect the chosen
behavior and ensure enabled is computed consistently from token+host presence.

Comment on lines +5 to +11
tenant_id LowCardinality(String),
date Date,
pathname String,
country LowCardinality(String),
region LowCardinality(String),
city LowCardinality(String),
visits AggregateFunction(uniq, String)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Realign this schema with the shared Tinybird definitions.
TABLE_DEFINITIONS.analytics_pages_mv in scripts/analytics/shared.js still lists a single location column, but this datasource now exposes country, region, and city. The new check-analytics validator will flag the mismatch and any automated provisioning scripts will fail. Update the shared table definition (and any other schema references) to reflect the new columns before landing this change.

🤖 Prompt for AI Agents
In scripts/analytics/tinybird/datasources/analytics_pages_mv.datasource around
lines 5 to 11, the datasource schema now exposes country, region, and city but
the shared TABLE_DEFINITIONS.analytics_pages_mv in scripts/analytics/shared.js
still declares a single location column; update that shared definition to
replace the single location column with LowCardinality(String) country, region,
and city (and keep other types/columns consistent), and then search the repo for
any other references to analytics_pages_mv or the location column (including
validation/config/provisioning scripts) and update them to use the new
country/region/city columns so the check-analytics validator and automated
provisioning will pass.

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: 4

Caution

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

⚠️ Outside diff range comments (1)
packages/web-backend/src/Videos/VideosRpcs.ts (1)

111-123: Move provideOptionalAuth before Effect.map() to fix the type inference error.

The getAnalyticsBulk handler applies provideOptionalAuth after Effect.map, unlike similar handlers such as GetUploadProgress which apply it before any transformations. The provideOptionalAuth function modifies the error channel by adding DatabaseError | UnknownException, which must be applied before downstream operators for proper type inference.

Reorder the pipeline:

VideosGetAnalytics: (videoIds) =>
  videos.getAnalyticsBulk(videoIds).pipe(
    provideOptionalAuth,
    Effect.map((results) => results.map((result) => Unify.unify(result))),
    Effect.catchTag(
      "DatabaseError",
      () => new InternalError({ type: "database" }),
    ),
    Effect.catchTag(
      "UnknownException",
      () => new InternalError({ type: "unknown" }),
    ),
  ),
♻️ Duplicate comments (13)
apps/web/app/api/analytics/track/route.ts (2)

25-106: Rebuild the route with HttpApi pattern
This still exports a bare POST handler and invokes runPromise directly, bypassing the required @effect/platform HttpApi builder + apiToHandler flow for apps/web/app/api/*. Please refactor the route into the shared HttpApi describe/create pattern so we inherit the standard middleware and per-request wiring.
Based on learnings


47-100: Guard against invalid occurredAt before toISOString
If a client sends an invalid timestamp, new Date(body.occurredAt) becomes Invalid Date and timestamp.toISOString() throws, returning a 500. Add an explicit guard that returns a 400 when getTime() is NaN.

- const timestamp = body.occurredAt ? new Date(body.occurredAt) : new Date();
+ const rawTimestamp = body.occurredAt ? new Date(body.occurredAt) : new Date();
+ if (Number.isNaN(rawTimestamp.getTime())) {
+   return Response.json(
+     { error: "occurredAt must be a valid ISO timestamp" },
+     { status: 400 },
+   );
+ }
+ const timestamp = rawTimestamp;
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)

94-95: The issues identified in previous reviews remain unresolved.

The past review comments accurately identify two problems with this function:

  1. Misleading parameter name: ignoreParams suggests query parameter handling, but it actually controls exact (pathname === path) vs. substring (pathname.includes(path)) pathname matching.

  2. Multiple active items: The pathname.includes(path) logic causes overlapping matches. For example, when viewing /dashboard/caps/record, both "My Caps" (/dashboard/caps) and "Record a Cap" (/dashboard/caps/record) will be active simultaneously, breaking the layoutId="navlinks" animation.

Consider the approach suggested in the previous review—rename the parameter to reflect its actual purpose (e.g., exact) and use pathname.startsWith(path + '/') or segment-based matching instead of includes:

-const isPathActive = (path: string, ignoreParams: boolean = false) =>
-  ignoreParams ? pathname === path : pathname.includes(path);
+const isPathActive = (path: string, exact: boolean = false) => {
+  if (exact) return pathname === path;
+  return pathname === path || pathname.startsWith(path + '/');
+};

Then update the navigation configuration and all call sites accordingly.

packages/ui/src/components/Select.tsx (2)

205-208: Guard icon cloning and preserve caller classes.

icon is typed as React.ReactNode, so any consumer passing a string/number/fragment will trigger a runtime error when calling React.cloneElement. Additionally, the code overwrites any existing className set by the caller. Please gate the clone behind React.isValidElement and merge their classes.

Apply this diff:

-      {icon &&
-        React.cloneElement(icon as React.ReactElement<{ className: string }>, {
-          className: cx(iconSizeVariant[size], "text-gray-9"),
-        })}
+      {React.isValidElement(icon)
+        ? React.cloneElement(icon, {
+            className: cx(iconSizeVariant[size], "text-gray-9", icon.props.className),
+          })
+        : null}

290-294: Apply the same safe icon handling here.

The same two issues occur for item icons: cloning a generic React.ReactNode will crash for valid inputs, and we drop any caller-supplied className. Mirror the guarded merge pattern.

Apply this diff:

-      {icon &&
-        React.cloneElement(
-          icon as React.ReactElement<{ className: string }>,
-          { className: cx("size-3", "text-gray-9") },
-        )}
+      {React.isValidElement(icon)
+        ? React.cloneElement(icon, {
+            className: cx("size-3", "text-gray-9", icon.props.className),
+          })
+        : null}
packages/web-backend/src/Tinybird/index.ts (1)

34-49: Don’t crash when Tinybird env vars are absent.

Right now we die if TINYBIRD_HOST is missing, while TINYBIRD_TOKEN just disables the client. Because Tinybird.Default is now wired into the global Dependencies, any environment that hasn’t set TINYBIRD_HOST (fresh dev shells, CI, QA) will fail to boot the entire app. Please align the validation so the service simply disables itself when either value is absent (or require both together) instead of crashing the runtime. Something along these lines keeps the app running while still logging why analytics is disabled:

- if (!host) {
-   yield* Effect.die(new Error("TINYBIRD_HOST must be set"));
- }
-
- const enabled = Boolean(token);
-
- if (!enabled) {
-   yield* Effect.logWarning(
-     "Tinybird is disabled: TINYBIRD_TOKEN is not set",
-   );
- }
+ const enabled = Boolean(host && token);
+
+ if (!enabled) {
+   const missing = [
+     !host ? "TINYBIRD_HOST" : null,
+     !token ? "TINYBIRD_TOKEN" : null,
+   ].filter(Boolean);
+   yield* Effect.logWarning(
+     `Tinybird is disabled: ${missing.join(", ")} not set`,
+   );
+ }

This mirrors the earlier feedback and prevents a full outage when analytics isn’t configured.

scripts/analytics/populate-test-data.js (1)

36-42: Math.random() is appropriate for test data generation.

The static analysis tool flags this as "insecure randomness," but Math.random() is perfectly acceptable for generating test analytics data. Cryptographically secure randomness is not required in this context.

apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx (1)

1-256: Run formatter to fix formatting issues.

The pipeline indicates a formatting error on line 54. Please run the formatter as per the coding guidelines.

#!/bin/bash
# Run biome formatter on the file
pnpm format
apps/web/lib/Queries/Analytics.ts (1)

36-38: Fix the request construction.

Line 37 incorrectly constructs the request as new AnalyticsRequest.AnalyticsRequest({ videoId }). Since AnalyticsRequest is already the exported request class, this should be new AnalyticsRequest({ videoId }).

Apply this diff to fix the construction:

 						uniqueVideoIds.map((videoId) =>
 							Effect.request(
-								new AnalyticsRequest.AnalyticsRequest({ videoId }),
+								new AnalyticsRequest({ videoId }),
 								dataloader,
 							).pipe(
apps/web/app/(org)/dashboard/analytics/data.ts (2)

95-118: Remove verbose Tinybird logging before release.

The withTinybirdFallback helper logs every raw Tinybird response, data array, and filtered rows (lines 104-115). In production, this will:

  • Dump sensitive analytics data (countries, cities, devices, user sessions) to stdout
  • Overwhelm logs with large JSON payloads
  • Degrade performance

Apply this diff to remove or gate the verbose logging:

 	effect.pipe(
 		Effect.catchAll((e) => {
 			console.error("tinybird query error", e);
 			return Effect.succeed<{ data: Row[] }>({ data: [] as Row[] });
 		}),
 		Effect.map((res) => {
-			console.log("tinybird raw response", JSON.stringify(res, null, 2));
 			const response = res as { data: unknown[] };
 			const data = response.data ?? [];
-			console.log("tinybird data array", JSON.stringify(data, null, 2));
 			const filtered = data.filter((item): item is Row => {
-				const isObject = typeof item === "object" && item !== null;
-				if (!isObject) {
-					console.log("filtered out non-object item", typeof item, item);
-				}
-				return isObject;
+				return typeof item === "object" && item !== null;
 			}) as Row[];
-			console.log("tinybird filtered rows", JSON.stringify(filtered, null, 2));
 			return filtered;
 		}),
 	);

199-301: Remove debug logging from the main data orchestration function.

Lines 202-289 contain multiple console.log statements that dump the entire analytics payload (orgId, range, timestamps, view series, countries, cities, browsers, devices, OS, and top caps). This verbose logging should be removed before merging.

Apply this diff to remove the debug logs:

 	const tinybirdData = await runPromise(
 		Effect.gen(function* () {
 			const tinybird = yield* Tinybird;
-			console.log("getOrgAnalyticsData - orgId:", orgId, "range:", range);
-			console.log(
-				"getOrgAnalyticsData - from:",
-				from.toISOString(),
-				"to:",
-				to.toISOString(),
-			);

 			const viewSeries = yield* queryViewSeries(
 				tinybird,
 				typedOrgId,
 				from,
 				to,
 				rangeConfig.bucket,
 				videoIds,
 			);
-			console.log(
-				"getOrgAnalyticsData - viewSeries:",
-				JSON.stringify(viewSeries, null, 2),
-			);

 			const countries = yield* queryCountries(
 				tinybird,
 				typedOrgId,
 				from,
 				to,
 				videoIds,
 			);
-			console.log(
-				"getOrgAnalyticsData - countries:",
-				JSON.stringify(countries, null, 2),
-			);

 			const cities = yield* queryCities(
 				tinybird,
 				typedOrgId,
 				from,
 				to,
 				videoIds,
 			);
-			console.log(
-				"getOrgAnalyticsData - cities:",
-				JSON.stringify(cities, null, 2),
-			);

 			const browsers = yield* queryBrowsers(
 				tinybird,
 				typedOrgId,
 				from,
 				to,
 				videoIds,
 			);
-			console.log(
-				"getOrgAnalyticsData - browsers:",
-				JSON.stringify(browsers, null, 2),
-			);

 			const devices = yield* queryDevices(
 				tinybird,
 				typedOrgId,
 				from,
 				to,
 				videoIds,
 			);
-			console.log(
-				"getOrgAnalyticsData - devices:",
-				JSON.stringify(devices, null, 2),
-			);

 			const operatingSystems = yield* queryOperatingSystems(
 				tinybird,
 				typedOrgId,
 				from,
 				to,
 				videoIds,
 			);
-			console.log(
-				"getOrgAnalyticsData - operatingSystems:",
-				JSON.stringify(operatingSystems, null, 2),
-			);

 			const topCapsRaw = capId
 				? []
 				: yield* queryTopCaps(tinybird, typedOrgId, from, to, videoIds);
-			console.log(
-				"getOrgAnalyticsData - topCapsRaw:",
-				JSON.stringify(topCapsRaw, null, 2),
-			);

 			return {
apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx (1)

105-120: Map Android (and other OS values) correctly.

The previous review comment about Android mapping is still valid. This function defaults any unrecognized OS to "windows", causing Android, Chrome OS, and other platforms to display incorrectly.

Additionally, note that OperatingSystemType in TableCard.tsx only includes "windows" | "ios" | "linux" | "fedora" | "ubuntu". You'll need to:

  1. Add "android" to the OperatingSystemType union in TableCard.tsx
  2. Ensure the corresponding icon asset exists at /logos/os/android.svg
  3. Update this mapping function per the previous review
 const osNameToKey = (name: string): OSRowData["os"] => {
 	const normalized = name.toLowerCase().trim();
 	if (normalized.includes("mac") || normalized === "ios") {
 		return "ios";
 	}
 	switch (normalized) {
+		case "android":
+			return "android";
 		case "linux":
 			return "linux";
 		case "ubuntu":
 			return "ubuntu";
 		case "fedora":
 			return "fedora";
 		default:
 			return "windows";
 	}
 };
apps/web/app/(org)/dashboard/analytics/components/Header.tsx (1)

303-345: Restore organization switching capability.

The previous review comment is still valid. The dropdown only renders the active organization and its spaces, preventing users from switching to other organizations they belong to. This is a critical UX regression for multi-org users.

As suggested in the previous comment, iterate the organizations array and render a SelectPrimitive.Item for each organization. Consider adding an "Organizations" section header similar to the "Spaces" section:

 <SelectPrimitive.Viewport className="p-1">
+  <div className="px-3 py-2 text-xs font-semibold text-gray-9 uppercase tracking-wider">
+    Organizations
+  </div>
+  {organizations?.map((org) => (
+    <SelectPrimitive.Item
+      key={org.organization.id}
+      value={org.organization.id}
+      className="relative flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none transition-colors text-gray-12 data-[state=checked]:bg-gray-3 data-[highlighted]:bg-gray-3 data-[highlighted]:text-gray-12"
+    >
+      <SignedImageUrl
+        image={org.organization.iconUrl}
+        name={org.organization.name}
+        className="size-5 flex-shrink-0"
+      />
+      <SelectPrimitive.ItemText>
+        {org.organization.name}
+      </SelectPrimitive.ItemText>
+    </SelectPrimitive.Item>
+  ))}
+  <SelectPrimitive.Separator className="h-px bg-gray-4 my-1" />
   <SelectPrimitive.Item
     key={activeOrganization.organization.id}
     value={activeOrganization.organization.id}
-    className="relative flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none transition-colors font-semibold text-gray-12 data-[state=checked]:bg-gray-3 data-[highlighted]:bg-gray-3 data-[highlighted]:text-gray-12"
+    className="relative flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none transition-colors text-gray-12 data-[state=checked]:bg-gray-3 data-[highlighted]:bg-gray-3 data-[highlighted]:text-gray-12"
   >
     <SignedImageUrl
       image={
         user?.imageUrl || activeOrganization.organization.iconUrl
       }
       name={user?.name || activeOrganization.organization.name}
       className="size-5 flex-shrink-0"
     />
     <SelectPrimitive.ItemText>
       {user?.name ? `${user.name}'s Caps` : "My Caps"}
     </SelectPrimitive.ItemText>
   </SelectPrimitive.Item>
🧹 Nitpick comments (7)
apps/web/app/api/analytics/route.ts (1)

30-32: Remove noisy debug logs before shipping.

The console.log statements will spam output on every request and don’t add lasting value. Please drop them before merge to keep server logs clean.

scripts/analytics/populate-test-data.js (1)

116-134: Document the SSL configuration for clarity.

The rejectUnauthorized: false setting on line 129 is suitable for test/development environments but may cause confusion. Consider adding a comment explaining that this allows self-signed certificates for local/test database connections.

Apply this diff to add clarifying documentation:

 	const config = {
 		host: parsed.hostname,
 		port: parsed.port ? parseInt(parsed.port, 10) : 3306,
 		user: parsed.username,
 		password: parsed.password,
 		database: parsed.pathname.slice(1),
 		ssl: {
+			// Allow self-signed certificates for test/dev environments
 			rejectUnauthorized: false,
 		},
 	};
packages/web-backend/src/Videos/index.ts (1)

77-90: Guard against potential undefined access.

Line 86 accesses videoIds[index] which could theoretically be undefined if the videoIds array and videoExits array lengths diverge. While this shouldn't happen in practice, adding a safety check would prevent runtime errors.

Apply this diff to add a guard:

 				for (let index = 0; index < videoExits.length; index++) {
 					const exit = videoExits[index];
+					const videoId = videoIds[index];
-					if (!exit) continue;
+					if (!exit || !videoId) continue;
 					if (Exit.isSuccess(exit)) {
 						const maybeVideo = exit.value;
 						if (Option.isSome(maybeVideo)) {
 							const [video] = maybeVideo.value;
 							successfulVideos.push({
 								index,
-								videoId: videoIds[index] ?? "",
+								videoId,
 								video,
 							});
 						}
 					}
 				}
apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx (2)

162-177: Remove commented-out code.

This commented filter dropdown UI adds clutter without providing value. If the feature is planned for later, track it in an issue instead.

Apply this diff to remove the dead code:

-			{/* <div className="flex flex-1 gap-2 justify-between items-center h-[48px]">
-        <p className="text-lg font-medium text-gray-12">{title}</p>
-        <Select
-          variant="light"
-          placeholder="Views"
-          icon={<FontAwesomeIcon icon={faFilter} className="text-gray-11" />}
-          options={[
-            { value: "views", label: "Views" },
-            { value: "comments", label: "Comments" },
-            { value: "reactions", label: "Reactions" },
-          ]}
-          value="views"
-          onValueChange={() => {}}
-          size="sm"
-        />
-      </div> */}

24-29: Add defensive validation for country codes to fail-fast on unexpected data.

The data flows from Tinybird's analytics backend (trusted IP geolocation source), where it undergoes no transformation that could corrupt valid ISO codes. However, getUnicodeFlagIcon silently produces emoji for any two-letter input without validation, meaning invalid codes won't throw but will render unexpected flags. Adding a length check is a low-cost defensive measure:

const countryCodeToIcon = (countryCode: string | undefined | null) => {
	if (!countryCode || countryCode.trim() === "") {
		return null;
	}
+	if (countryCode.length !== 2) {
+		return null;
+	}
	return getUnicodeFlagIcon(countryCode.toUpperCase());
};
apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx (2)

37-42: Align component name with filename.

The file is named StatsChart.tsx but exports StatsBox. This inconsistency can cause confusion when importing and navigating the codebase.

Either rename the function to StatsChart or rename the file to StatsBox.tsx:

-export default function StatsBox({
+export default function StatsChart({
 	counts,
 	data,
 	isLoading,
 	capId,
 }: StatsChartProps) {

197-212: Consider CSS variables instead of inline style manipulation.

Directly manipulating style.backgroundColor in event handlers makes the styling logic harder to maintain and test. CSS variables or state-driven class names would be cleaner.

Example refactor using CSS variables:

style={{
  '--metric-bg': isSelected ? colors.bgSelected : colors.bg,
  '--metric-bg-hover': colors.bgHover,
  '--metric-border': isSelected ? colors.borderSelected : colors.border,
} as React.CSSProperties}
className={classNames(
  "transition-all [background-color:var(--metric-bg)] [border-color:var(--metric-border)]",
  "hover:[background-color:var(--metric-bg-hover)]",
  // ...
)}

Then remove the onMouseEnter/onMouseLeave style mutations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4838494 and 2b48e50.

📒 Files selected for processing (31)
  • apps/web/actions/videos/get-analytics.ts (1 hunks)
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (8 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/Header.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/analytics/data.ts (1 hunks)
  • apps/web/app/(org)/dashboard/caps/Caps.tsx (5 hunks)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/caps/page.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/folder/[id]/page.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx (2 hunks)
  • apps/web/app/api/analytics/route.ts (1 hunks)
  • apps/web/app/api/analytics/track/route.ts (1 hunks)
  • apps/web/app/api/dashboard/analytics/route.ts (1 hunks)
  • apps/web/app/s/[videoId]/Share.tsx (5 hunks)
  • apps/web/lib/Queries/Analytics.ts (1 hunks)
  • apps/web/lib/server.ts (2 hunks)
  • packages/ui/src/components/Select.tsx (9 hunks)
  • packages/web-backend/src/Tinybird/index.ts (1 hunks)
  • packages/web-backend/src/Videos/VideosRpcs.ts (1 hunks)
  • packages/web-backend/src/Videos/index.ts (4 hunks)
  • packages/web-backend/src/index.ts (1 hunks)
  • scripts/analytics/check-analytics.js (1 hunks)
  • scripts/analytics/delete-all-data.js (1 hunks)
  • scripts/analytics/populate-test-data.js (1 hunks)
  • scripts/analytics/setup-analytics.js (1 hunks)
  • scripts/analytics/shared.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx
  • scripts/analytics/check-analytics.js
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx
  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
  • scripts/analytics/delete-all-data.js
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Files:

  • apps/web/lib/server.ts
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/actions/videos/get-analytics.ts
  • apps/web/app/(org)/dashboard/folder/[id]/page.tsx
  • apps/web/lib/Queries/Analytics.ts
  • packages/web-backend/src/Videos/VideosRpcs.ts
  • apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx
  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx
  • apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx
  • packages/web-backend/src/Tinybird/index.ts
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
  • apps/web/app/(org)/dashboard/analytics/data.ts
  • apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx
  • apps/web/app/(org)/dashboard/analytics/components/Header.tsx
  • packages/ui/src/components/Select.tsx
  • packages/web-backend/src/index.ts
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/api/dashboard/analytics/route.ts
  • packages/web-backend/src/Videos/index.ts
  • apps/web/app/api/analytics/route.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/web/lib/server.ts
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/actions/videos/get-analytics.ts
  • apps/web/app/(org)/dashboard/folder/[id]/page.tsx
  • apps/web/lib/Queries/Analytics.ts
  • packages/web-backend/src/Videos/VideosRpcs.ts
  • apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx
  • apps/web/app/api/analytics/track/route.ts
  • scripts/analytics/setup-analytics.js
  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx
  • apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx
  • packages/web-backend/src/Tinybird/index.ts
  • scripts/analytics/shared.js
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
  • apps/web/app/(org)/dashboard/analytics/data.ts
  • scripts/analytics/populate-test-data.js
  • apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx
  • apps/web/app/(org)/dashboard/analytics/components/Header.tsx
  • packages/ui/src/components/Select.tsx
  • packages/web-backend/src/index.ts
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/api/dashboard/analytics/route.ts
  • packages/web-backend/src/Videos/index.ts
  • apps/web/app/api/analytics/route.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

On the client, always use useEffectQuery or useEffectMutation from @/lib/EffectRuntime; never call EffectRuntime.run* directly in components.

Files:

  • apps/web/lib/server.ts
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/actions/videos/get-analytics.ts
  • apps/web/app/(org)/dashboard/folder/[id]/page.tsx
  • apps/web/lib/Queries/Analytics.ts
  • apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx
  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx
  • apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
  • apps/web/app/(org)/dashboard/analytics/data.ts
  • apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx
  • apps/web/app/(org)/dashboard/analytics/components/Header.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/api/dashboard/analytics/route.ts
  • apps/web/app/api/analytics/route.ts
packages/ui/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Component files in packages/ui should use PascalCase naming if they define React/Solid components.

Files:

  • packages/ui/src/components/Select.tsx
🧠 Learnings (5)
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : API routes in Next.js (`apps/web/app/api/*`) must use `effect/platform`'s `HttpApi` builder and follow the existing class/group/endpoint pattern; avoid ad-hoc handlers.

Applied to files:

  • apps/web/app/api/analytics/track/route.ts
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : Convert effectful APIs to Next.js handlers using `apiToHandler(ApiLive)` and export only the returned `handler`; avoid calling `runPromise` directly in route files.

Applied to files:

  • apps/web/app/api/analytics/track/route.ts
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : On the server, always run effects through `EffectRuntime.runPromise` after `provideOptionalAuth` to ensure cookies and per-request context are attached.

Applied to files:

  • apps/web/app/api/analytics/track/route.ts
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to **/*.{ts,tsx} : Use Biome for formatting and linting TypeScript/JavaScript files by running `pnpm format`.

Applied to files:

  • apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx
  • packages/web-backend/src/index.ts
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/**/*.{ts,tsx,js,jsx} : On the client, always use `useEffectQuery` or `useEffectMutation` from `@/lib/EffectRuntime`; never call `EffectRuntime.run*` directly in components.

Applied to files:

  • apps/web/app/s/[videoId]/Share.tsx
🧬 Code graph analysis (19)
apps/web/lib/server.ts (2)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-276)
packages/web-backend/src/index.ts (1)
  • Tinybird (14-14)
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (1)
  • ChartLineIcon (27-27)
apps/web/actions/videos/get-analytics.ts (5)
packages/database/index.ts (1)
  • db (18-25)
packages/database/schema.ts (1)
  • videos (291-363)
packages/web-domain/src/Video.ts (1)
  • Video (16-59)
apps/web/lib/server.ts (1)
  • runPromise (141-153)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-276)
apps/web/lib/Queries/Analytics.ts (2)
apps/web/lib/Requests/AnalyticsRequest.ts (1)
  • AnalyticsRequest (8-12)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectQuery (22-22)
apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx (2)
packages/ui/src/components/Table.tsx (6)
  • Table (117-117)
  • TableHeader (123-123)
  • TableRow (124-124)
  • TableHead (122-122)
  • TableBody (118-118)
  • TableCell (120-120)
packages/ui/src/components/icons/LogoBadge.tsx (1)
  • LogoBadge (1-28)
apps/web/app/api/analytics/track/route.ts (3)
apps/web/lib/server.ts (1)
  • runPromise (141-153)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-276)
packages/web-backend/src/Auth.ts (1)
  • provideOptionalAuth (102-114)
scripts/analytics/setup-analytics.js (2)
scripts/analytics/migrate-dub-to-tinybird.js (1)
  • __dirname (8-8)
scripts/analytics/shared.js (2)
  • PROJECT_ROOT (9-9)
  • auth (140-140)
apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx (1)
apps/web/components/ui/chart.tsx (4)
  • ChartConfig (10-18)
  • ChartContainer (354-354)
  • ChartTooltip (355-355)
  • ChartTooltipContent (356-356)
apps/web/app/(org)/dashboard/analytics/components/StatsChart.tsx (2)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Cap.tsx (1)
  • CapIconHandle (8-11)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (4)
  • ClapIcon (28-28)
  • ChatIcon (29-29)
  • ReactionIcon (30-30)
  • CapIcon (18-18)
packages/web-backend/src/Tinybird/index.ts (4)
packages/env/server.ts (1)
  • serverEnv (136-140)
scripts/analytics/shared.js (2)
  • request (142-177)
  • raw (92-92)
scripts/analytics/populate-test-data.js (8)
  • url (94-94)
  • response (95-103)
  • i (170-170)
  • i (211-211)
  • i (225-225)
  • i (229-229)
  • text (104-104)
  • search (93-93)
scripts/analytics/check-analytics.js (4)
  • errorMessage (146-147)
  • current (27-27)
  • depth (26-26)
  • parts (25-25)
scripts/analytics/shared.js (3)
scripts/analytics/populate-test-data.js (6)
  • os (62-62)
  • auth (218-218)
  • url (94-94)
  • response (95-103)
  • text (104-104)
  • payload (108-108)
scripts/analytics/migrate-dub-to-tinybird.js (11)
  • os (5480-5480)
  • data (4563-4563)
  • data (4641-4641)
  • data (4734-4734)
  • url (4438-4438)
  • url (4827-4827)
  • response (4443-4445)
  • response (4828-4836)
  • text (4449-4449)
  • text (4837-4837)
  • payload (4462-4462)
scripts/analytics/delete-all-data.js (2)
  • auth (78-78)
  • payload (8-8)
apps/web/app/(org)/dashboard/caps/Caps.tsx (5)
apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx (1)
  • VideoData (37-50)
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (1)
  • FolderDataType (18-25)
packages/web-domain/src/Video.ts (3)
  • Video (16-59)
  • VideoId (12-12)
  • VideoId (13-13)
apps/web/lib/EffectRuntime.ts (2)
  • useRpcClient (25-25)
  • useEffectMutation (23-23)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)
  • CapCard (101-648)
apps/web/app/(org)/dashboard/analytics/data.ts (5)
packages/database/schema.ts (3)
  • videos (291-363)
  • spaceVideos (665-688)
  • comments (394-420)
apps/web/app/(org)/dashboard/analytics/types.ts (3)
  • AnalyticsRange (1-1)
  • OrgAnalyticsResponse (10-33)
  • BreakdownRow (3-8)
packages/database/index.ts (1)
  • db (18-25)
apps/web/lib/server.ts (1)
  • runPromise (141-153)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-276)
scripts/analytics/populate-test-data.js (2)
scripts/analytics/migrate-dub-to-tinybird.js (6)
  • TB_DATASOURCE (16-16)
  • INGEST_CHUNK_SIZE (18-18)
  • i (4117-4117)
  • i (4181-4181)
  • i (4805-4805)
  • i (5610-5610)
scripts/analytics/shared.js (1)
  • auth (140-140)
apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx (3)
apps/web/app/(org)/dashboard/analytics/types.ts (1)
  • BreakdownRow (3-8)
apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx (6)
  • DeviceRowData (100-107)
  • CountryRowData (64-71)
  • CityRowData (73-80)
  • BrowserRowData (82-89)
  • OSRowData (91-98)
  • CapRowData (109-116)
apps/web/app/(org)/dashboard/analytics/components/OtherStatBox.tsx (1)
  • OtherStatBox (12-32)
apps/web/app/(org)/dashboard/analytics/components/Header.tsx (4)
apps/web/app/(org)/dashboard/analytics/types.ts (1)
  • AnalyticsRange (1-1)
packages/web-domain/src/Organisation.ts (1)
  • Organisation (21-24)
apps/web/app/Layout/AuthContext.tsx (1)
  • useCurrentUser (40-42)
apps/web/components/SignedImageUrl.tsx (1)
  • SignedImageUrl (11-25)
apps/web/app/api/dashboard/analytics/route.ts (2)
apps/web/app/(org)/dashboard/analytics/types.ts (1)
  • AnalyticsRange (1-1)
apps/web/app/(org)/dashboard/analytics/data.ts (1)
  • getOrgAnalyticsData (128-379)
packages/web-backend/src/Videos/index.ts (4)
packages/web-domain/src/Video.ts (3)
  • Video (16-59)
  • VideoId (12-12)
  • VideoId (13-13)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-276)
packages/web-backend/src/Videos/VideosPolicy.ts (1)
  • VideosPolicy (9-96)
packages/web-backend/src/Videos/VideosRepo.ts (1)
  • VideosRepo (15-106)
apps/web/app/api/analytics/route.ts (2)
apps/web/app/api/dashboard/analytics/route.ts (1)
  • GET (11-43)
apps/web/actions/videos/get-analytics.ts (1)
  • getVideoAnalytics (34-96)
🪛 GitHub Actions: CI
packages/web-backend/src/Videos/VideosRpcs.ts

[error] 8-8: TypeScript error TS2345: Argument of type 'Effect<{ VideoDelete: (videoId: string & Brand<"VideoId">) => Effect<undefined, InternalError | PolicyDeniedError | NotFoundError, CurrentUser>; ... 6 more ...; VideosGetAnalytics: (videoIds: readonly (string & Brand<...>)[]) => Effect<...>; }, never, Videos>' is not assignable to parameter of type 'HandlersFrom<Rpc<"VideoDelete", brand<typeof String$, "VideoId">, typeof Void, Union<[typeof NotFoundError, typeof InternalError, typeof PolicyDeniedError]>, typeof RpcAuthMiddleware> | ... 6 more ... | Rpc<...>> | Effect<...>'.

🪛 GitHub Check: CodeQL
scripts/analytics/populate-test-data.js

[failure] 63-69: Insecure randomness
This uses a cryptographically insecure random number generated at Math.random() in a security context.

🔇 Additional comments (18)
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (2)

39-39: LGTM!

The ChartLineIcon import is correctly added to support the new Analytics navigation item.


62-68: LGTM!

The Analytics navigation item is correctly structured and positioned. The use of ignoreParams: true ensures this item is only active on the exact /dashboard/analytics route, not on potential sub-routes.

packages/ui/src/components/Select.tsx (7)

1-12: LGTM!

The imports, type definitions, and context setup are well-structured and appropriate for a variant-driven component system.


13-102: LGTM!

The CVA variant configurations are well-structured with consistent variant names across trigger, content, and item components. The class compositions handle states and transitions appropriately.


104-159: LGTM!

The component composition and prop threading are correct. The variant system integration is clean and the options mapping handles both icons and images appropriately.


161-177: LGTM!

The wrapper components appropriately delegate to Radix primitives with proper data-slot attributes.


217-252: LGTM!

The SelectContent properly provides variant context to descendant items, enabling variant-aware styling throughout the component tree.


254-265: LGTM!

The label component correctly applies typography and spacing classes.


300-361: LGTM!

The separator and scroll button components are implemented correctly with appropriate styling. The exports are comprehensive and well-organized.

apps/web/app/(org)/dashboard/folder/[id]/page.tsx (1)

87-89: LGTM!

The migration from DUB-based analytics to Tinybird is correctly implemented. The prop name change to analyticsEnabled and the check for both TINYBIRD_TOKEN and TINYBIRD_HOST ensure analytics are only enabled when the backend service is properly configured.

apps/web/app/api/dashboard/analytics/route.ts (1)

11-43: LGTM!

The route handler correctly implements:

  • Authentication (401 for unauthenticated users)
  • Authorization (403 if requesting analytics for a different org)
  • Input validation (400 for missing org, validated range with fallback)
  • Error handling (500 with logging on data fetch failure)

The logic is clean and secure.

apps/web/app/(org)/dashboard/analytics/components/ChartArea.tsx (2)

52-64: LGTM!

The bucket duration detection logic is well-implemented. It correctly distinguishes between hourly and daily buckets by comparing the time difference between consecutive data points, enabling appropriate label formatting.


66-107: LGTM!

The Y-axis scaling algorithm properly computes "nice" tick values by:

  1. Finding the max value across selected metrics
  2. Rounding up by 10% for padding
  3. Normalizing to clean magnitudes (1, 2, 5, or 10)
  4. Generating evenly-spaced ticks

This ensures readable, user-friendly axis labels.

packages/web-backend/src/Videos/index.ts (2)

51-91: LGTM! The bulk analytics architecture is well-designed.

The implementation correctly:

  • Fetches videos with proper access control via getByIdForViewing
  • Groups videos by organization for efficient Tinybird queries
  • Uses Exit to preserve per-video success/failure states
  • Handles concurrent video fetches with bounded concurrency

113-127: LGTM! Error handling is robust.

The runTinybirdQuery helper gracefully handles Tinybird failures by:

  • Catching all errors and logging them
  • Returning an empty data array as a fallback
  • Ensuring the analytics flow continues even if Tinybird is unavailable

This prevents cascading failures.

apps/web/app/(org)/dashboard/analytics/data.ts (1)

128-379: LGTM! The analytics orchestration is well-architected.

The getOrgAnalyticsData function effectively:

  • Computes date ranges and buckets based on the selected analytics range
  • Handles optional space/cap filtering with early returns for empty results
  • Fetches data from both relational DB (videos, comments, reactions) and Tinybird (views, breakdowns) in parallel
  • Normalizes and aggregates breakdown data with proper percentage calculations
  • Resolves video names for display
  • Returns a complete, well-typed analytics response

The overall structure is solid despite the logging issues noted separately.

apps/web/app/(org)/dashboard/caps/Caps.tsx (1)

79-170: Well-structured analytics integration and deletion flow.

The analytics integration with analyticsEnabled and the new Effect-based deletion mutations are well-implemented:

  • Proper conditional query execution based on analyticsEnabled
  • Batch deletion with controlled concurrency (10)
  • Comprehensive error handling with user feedback via toasts
  • Router refresh ensures UI consistency after mutations
apps/web/app/(org)/dashboard/analytics/components/Header.tsx (1)

46-88: Date range mapping logic is well-designed.

The bidirectional mapping between UI values (today, yesterday, wtd, mtd) and backend AnalyticsRange values (24h, 7d, 30d) properly preserves user intent via lastUISelection. This ensures the UI reflects what the user selected rather than ambiguously mapping backend values.

Comment on lines +40 to +47
const toCountryRow = (row: BreakdownRow): CountryRowData => ({
countryCode: row.name,
name: row.name,
views: row.views,
comments: null,
reactions: null,
percentage: row.percentage,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify country code format from analytics data.

toCountryRow assigns row.name to both countryCode and name fields. The countryCode field should contain valid ISO 3166-1 alpha-2 codes (e.g., "US", "GB") for flag rendering to work correctly. If row.name contains full country names instead, flag icons will fail to display.

Run this script to check what format the analytics data provides:


🏁 Script executed:

#!/bin/bash
# Check how country data is structured in the analytics pipeline
rg -n "countries.*=|country.*:" --type=ts -C5 -g 'apps/web/app/(org)/dashboard/analytics/**'

# Also check the Tinybird query definitions
fd -e pipe -e sql . | xargs rg "country" -C3

Length of output: 15028



---

🏁 Script executed:

```shell
#!/bin/bash
# Find queryCountries implementation
rg -n "queryCountries" --type=ts -A15 apps/web/app/(org)/dashboard/analytics/

# Find BreakdownRow type definition
rg -n "type BreakdownRow|interface BreakdownRow" --type=ts -A8 apps/web/app/(org)/dashboard/analytics/

Length of output: 164



---

🏁 Script executed:

```shell
#!/bin/bash
# Find queryCountries implementation - escape or avoid parentheses
fd -type f -name "*.ts" -path "*dashboard/analytics*" -exec rg -l "queryCountries" {} \;

# Find BreakdownRow type
fd -type f -name "*.ts" -path "*dashboard/analytics*" -exec rg -n "type BreakdownRow|interface BreakdownRow" {} +

Length of output: 341



---

🏁 Script executed:

```shell
#!/bin/bash
# Find queryCountries function in analytics files
rg -n "queryCountries\s*=" apps/web/app/ -A 20

# Find BreakdownRow type definition
rg -n "type BreakdownRow|interface BreakdownRow" apps/web/app/ -A 8

# Also search for where country data is queried
rg -n "queryCountries\|getCountries\|countries.*=" apps/web/app/\(org\)/dashboard/analytics/ -A 10

Length of output: 2204



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where country data is ingested/sourced in Tinybird
rg -n "country" scripts/analytics/tinybird/ -B2 -A2

# Check if there's any country name to code mapping
rg -n "country.*map\|countryCode\|country.*iso" apps/web/app/ --type=ts -i

# Check the full queryCountries implementation
cat apps/web/app/\(org\)/dashboard/analytics/data.ts | sed -n '642,700p'

Length of output: 4883



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where analytics events are tracked and country is set
rg -n "country\s*:" apps/web/ --type=ts -B3 -A1 | grep -A3 -B3 "track\|analytics\|event"

# Look for event payload construction
rg -n "{\s*action\|EVENT\|track" apps/web/ --type=ts -A5 | grep -A5 "country"

# Search for tracking implementation
fd -name "*track*" -type f apps/web/ --type=ts

Length of output: 688



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for where country field is populated in client tracking
rg "country" apps/web/lib/ -B2 -A2 --type=ts

# Look for analytics or tracking client code
fd "analytics\|track" apps/web/lib/ --type=ts -exec rg -l "country" {} \;

# Search for GeoIP or location detection libraries
rg "geoip\|geolocation\|country" apps/web/ --type=ts -l | head -20

Length of output: 169



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for where country is set in event payload
rg -n "country" apps/web/lib --type=typescript -B2 -A2

# Look for event sending/tracking code
rg -n "country" scripts/ -B2 -A2

# Find country field assignments broadly
rg -n "country\s*=" apps/ -B2 -A2 --type=typescript | head -50

Length of output: 29659


Fix toCountryRow to extract or map country codes instead of using country names.

The analytics pipeline sends full country names (e.g., "United States", "United Kingdom") to Tinybird's country field. The toCountryRow function currently assigns row.name directly to countryCode, but the flag renderer expects ISO 3166-1 alpha-2 codes (e.g., "US", "GB"). This will cause flag icons to fail rendering.

The toCityRow function uses row.subtitle for countryCode, which suggests a different data structure. You need to determine whether:

  1. Tinybird's country breakdown query should extract ISO codes instead of names, or
  2. toCountryRow should map country names to ISO codes before assigning to countryCode.

Comment on lines +85 to +103
const browserNameToSlug = (name: string): BrowserRowData["browser"] => {
switch (name.toLowerCase()) {
case "chrome":
return "google-chrome";
case "firefox":
return "firefox";
case "safari":
return "safari";
case "edge":
case "internet explorer":
return "explorer";
case "opera":
return "opera";
case "brave":
return "brave";
default:
return "google-chrome";
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Extend browser mapping to cover all BrowserType values.

browserNameToSlug only handles a subset of browsers but defaults to "google-chrome" for anything unrecognized. This will cause browsers like Vivaldi, Yandex, DuckDuckGo, Samsung Internet, UC Browser, QQ Browser, Maxthon, Arora, and Lunascape (all defined in BrowserType) to incorrectly display Chrome's icon.

Either extend the switch statement to handle all supported browsers, or verify which browsers Tinybird actually reports and adjust the BrowserType union accordingly:

 	switch (name.toLowerCase()) {
 		case "chrome":
 			return "google-chrome";
 		case "firefox":
 			return "firefox";
 		case "safari":
 			return "safari";
 		case "edge":
 		case "internet explorer":
 			return "explorer";
 		case "opera":
 			return "opera";
 		case "brave":
 			return "brave";
+		case "vivaldi":
+			return "vivaldi";
+		case "yandex":
+			return "yandex";
+		// ... add remaining browsers
 		default:
 			return "google-chrome";
 	}

Or run this script to see what browser names Tinybird is actually returning:

#!/bin/bash
# Search for browser data processing in analytics code
ast-grep --pattern 'browsers: $$$'
rg -n "browser.*name|browser.*:" --type=ts -C3 -g 'apps/web/app/(org)/dashboard/analytics/**'

Comment on lines +302 to +329
const OperatingSystemIcon = ({ operatingSystem }: OperatingSystemIconProps) => {
if (operatingSystem === "ios") {
return <FontAwesomeIcon className="text-gray-12" icon={faAppleWhole} />;
}
return (
<Image
src={`/logos/os/${operatingSystem}.svg`}
alt={operatingSystem}
width={16}
height={16}
className="size-4"
/>
);
};

type BrowserIconProps = {
browser: BrowserType;
};

const BrowserIcon = ({ browser }: BrowserIconProps) => (
<Image
src={`/logos/browsers/${browser}.svg`}
alt={browser}
width={16}
height={16}
className="size-4"
/>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify OS and browser icon assets exist.

The icon components assume SVG files exist at /logos/os/${operatingSystem}.svg and /logos/browsers/${browser}.svg. Missing assets will cause broken images or 404 errors.

Run this script to verify all expected assets are present:


🏁 Script executed:

#!/bin/bash
# Check for OS icon files
echo "=== Checking OS icons ==="
for os in windows linux fedora ubuntu; do
  if [ -f "apps/web/public/logos/os/${os}.svg" ]; then
    echo "${os}.svg exists"
  else
    echo "${os}.svg MISSING"
  fi
done

# Check for browser icon files
echo -e "\n=== Checking browser icons ==="
for browser in google-chrome firefox safari explorer opera brave vivaldi yandex duckduckgo internet-explorer samsung-internet uc-browser qq-browser maxthon arora lunascape; do
  if [ -f "apps/web/public/logos/browsers/${browser}.svg" ]; then
    echo "${browser}.svg exists"
  else
    echo "${browser}.svg MISSING"
  fi
done

Length of output: 509



🏁 Script executed:

#!/bin/bash
# Find BrowserType definition and enum values
echo "=== Finding BrowserType definition ==="
rg -n "type BrowserType|enum.*Browser|BrowserType\s*=" apps/web --type ts --type tsx -B2 -A10

# Check if any of the missing browser types are referenced in code
echo -e "\n=== Checking for references to missing browser types ==="
for browser in internet-explorer samsung-internet uc-browser qq-browser maxthon arora lunascape; do
  count=$(rg -c "$browser" apps/web --type ts --type tsx)
  if [ "$count" -gt 0 ]; then
    echo "✗ '$browser' is referenced $count times"
  else
    echo "✓ '$browser' not referenced"
  fi
done

Length of output: 905



🏁 Script executed:

#!/bin/bash
# Find BrowserType definition without specifying tsx separately
echo "=== Finding BrowserType definition ==="
rg -n "type BrowserType" apps/web -A 15

# Also search for where browser values are defined/used
echo -e "\n=== Looking for browser data or switch cases ==="
rg -n "browser.*==|case.*safari|case.*firefox" apps/web -C2 | head -50

# Check TableCard.tsx for browser type usage
echo -e "\n=== Checking TableCard.tsx context ==="
grep -n "BrowserType\|browser" apps/web/app/\(org\)/dashboard/analytics/components/TableCard.tsx | head -20

Length of output: 2455


Add missing browser icon SVG files or update BrowserType to exclude unsupported browsers.

The BrowserType union (lines 42-57) includes 17 browser types, but only 10 corresponding SVG files exist in apps/web/public/logos/browsers/. The 7 missing files are: internet-explorer.svg, samsung-internet.svg, uc-browser.svg, qq-browser.svg, maxthon.svg, arora.svg, and lunascape.svg.

The BrowserIcon component (lines 321-329) has no error handling for missing assets, so any of these browser types in the data will display broken images at runtime.

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/analytics/components/TableCard.tsx around lines
302 to 329: the BrowserIcon component assumes a matching SVG exists for every
BrowserType but seven browser SVGs are missing causing broken images; either add
the missing SVG files to apps/web/public/logos/browsers/ (internet-explorer.svg,
samsung-internet.svg, uc-browser.svg, qq-browser.svg, maxthon.svg, arora.svg,
lunascape.svg) or prune those values from the BrowserType union so only
supported browsers remain, and add a runtime fallback in BrowserIcon that
detects a missing asset and renders a generic/placeholder icon (or FontAwesome
fallback) instead of a broken <Image>; implement one of the two fixes (prefer
adding assets + fallback) and update any consumers or types accordingly.

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: 4

Caution

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

⚠️ Outside diff range comments (3)
packages/web-backend/src/Videos/VideosRpcs.ts (2)

111-139: Fix unsafe type assertion in error handling.

The bulk analytics refactoring is a good improvement over per-item fetching, but line 122 contains a critical type safety issue:

return error as Video.NotFoundError | Policy.PolicyDeniedError | Video.VerifyVideoPasswordError;

This catch-all branch forces any unhandled error into the union type without validation. If getAnalyticsBulk returns a different error type (e.g., DatabaseError, S3Error, or any other error), this assertion creates a type lie that could lead to incorrect error handling downstream.

Apply this diff to properly handle unknown errors:

-									return error as Video.NotFoundError | Policy.PolicyDeniedError | Video.VerifyVideoPasswordError;
+									// If we encounter an unexpected error type, fail the entire Effect
+									// rather than incorrectly coercing it
+									return Effect.die(
+										new Error(`Unexpected error type in analytics fetch: ${error}`)
+									);

Alternatively, if unknown errors should be treated as NotFoundError or another specific type, make that explicit:

-									return error as Video.NotFoundError | Policy.PolicyDeniedError | Video.VerifyVideoPasswordError;
+									// Treat unknown errors as NotFoundError
+									return new Video.NotFoundError();

Note: The explicit type annotation on lines 125-128 appears redundant and could be removed for cleaner code, as TypeScript should infer the type from the mapping logic.


1-142: Fix formatting to pass CI checks.

The CI pipeline detected a formatting issue. Please run the formatter to fix the code style:

pnpm format
apps/web/app/(org)/dashboard/caps/Caps.tsx (1)

1-1: Fix formatting to pass CI.

The pipeline is failing due to formatting issues. Run the formatter to resolve this.

pnpm format

As per coding guidelines.

🧹 Nitpick comments (3)
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)

55-61: Consider matchChildren: true for "My Caps" if detail views exist.

Currently, "My Caps" uses exact path matching, so it won't remain active when viewing individual cap details (e.g., /dashboard/caps/[id]). If such detail routes exist, adding matchChildren: true would keep the parent nav item highlighted when viewing child resources, which is a common UX pattern.

If /dashboard/caps/record is intentionally a sibling action (not a child view), the current setup is correct.

apps/web/app/s/[videoId]/page.tsx (2)

54-108: Consider combining queries for better performance.

The function performs two separate database queries and then merges the results client-side. This could be optimized by using a UNION query to combine both space-level and organization-level sharing in a single database round-trip.

Apply this refactor:

 async function getSharedSpacesForVideo(videoId: Video.VideoId) {
-  // Fetch space-level sharing
-  const spaceSharing = await db()
-    .select({
-      id: spaces.id,
-      name: spaces.name,
-      organizationId: spaces.organizationId,
-      iconUrl: organizations.iconUrl,
-    })
-    .from(spaceVideos)
-    .innerJoin(spaces, eq(spaceVideos.spaceId, spaces.id))
-    .innerJoin(organizations, eq(spaces.organizationId, organizations.id))
-    .where(eq(spaceVideos.videoId, videoId));
-
-  // Fetch organization-level sharing
-  const orgSharing = await db()
-    .select({
-      id: organizations.id,
-      name: organizations.name,
-      organizationId: organizations.id,
-      iconUrl: organizations.iconUrl,
-    })
-    .from(sharedVideos)
-    .innerJoin(organizations, eq(sharedVideos.organizationId, organizations.id))
-    .where(eq(sharedVideos.videoId, videoId));
-
-  const sharedSpaces: Array<{
-    id: string;
-    name: string;
-    organizationId: string;
-    iconUrl?: string;
-  }> = [];
-
-  // Add space-level sharing
-  spaceSharing.forEach((space) => {
-    sharedSpaces.push({
-      id: space.id,
-      name: space.name,
-      organizationId: space.organizationId,
-      iconUrl: space.iconUrl || undefined,
-    });
-  });
-
-  // Add organization-level sharing
-  orgSharing.forEach((org) => {
-    sharedSpaces.push({
-      id: org.id,
-      name: org.name,
-      organizationId: org.organizationId,
-      iconUrl: org.iconUrl || undefined,
-    });
-  });
-
-  return sharedSpaces;
+  // Combine both space-level and org-level sharing in a single query
+  const sharedSpaces = await db().execute(sql`
+    SELECT 
+      ${spaces.id} as id,
+      ${spaces.name} as name,
+      ${spaces.organizationId} as "organizationId",
+      ${organizations.iconUrl} as "iconUrl"
+    FROM ${spaceVideos}
+    INNER JOIN ${spaces} ON ${spaceVideos.spaceId} = ${spaces.id}
+    INNER JOIN ${organizations} ON ${spaces.organizationId} = ${organizations.id}
+    WHERE ${spaceVideos.videoId} = ${videoId}
+    
+    UNION
+    
+    SELECT 
+      ${organizations.id} as id,
+      ${organizations.name} as name,
+      ${organizations.id} as "organizationId",
+      ${organizations.iconUrl} as "iconUrl"
+    FROM ${sharedVideos}
+    INNER JOIN ${organizations} ON ${sharedVideos.organizationId} = ${organizations.id}
+    WHERE ${sharedVideos.videoId} = ${videoId}
+  `);
+
+  return sharedSpaces.map(space => ({
+    id: space.id,
+    name: space.name,
+    organizationId: space.organizationId,
+    iconUrl: space.iconUrl || undefined,
+  }));
 }

351-771: Consider breaking down this large function.

AuthorizedContent spans ~420 lines and handles multiple concerns: authentication, notifications, spaces data, AI settings, domain restrictions, transcription, metadata generation, and rendering. This violates the Single Responsibility Principle and makes the code harder to maintain and test.

Consider extracting logical sections into smaller focused functions:

  • fetchSpacesData(user) for lines 392-404
  • checkAiGenerationEnabled(videoOwnerId) for lines 406-419
  • validateDomainAccess(video, user) for lines 421-451
  • handleTranscription(video, aiGenerationEnabled) for lines 453-499
  • initializeAiMetadata(video, metadata, aiGenerationEnabled) for lines 501-542
  • fetchVideoRelatedData(videoId, userId, user, video) for lines 544-699

This would improve readability, testability, and maintainability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b48e50 and c321959.

📒 Files selected for processing (4)
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (8 hunks)
  • apps/web/app/(org)/dashboard/caps/Caps.tsx (2 hunks)
  • apps/web/app/s/[videoId]/page.tsx (3 hunks)
  • packages/web-backend/src/Videos/VideosRpcs.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Files:

  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • packages/web-backend/src/Videos/VideosRpcs.ts
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • packages/web-backend/src/Videos/VideosRpcs.ts
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

On the client, always use useEffectQuery or useEffectMutation from @/lib/EffectRuntime; never call EffectRuntime.run* directly in components.

Files:

  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
🧬 Code graph analysis (4)
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (1)
  • ChartLineIcon (27-27)
apps/web/app/s/[videoId]/page.tsx (13)
packages/database/index.ts (1)
  • db (18-25)
packages/database/schema.ts (5)
  • spaces (614-641)
  • organizations (173-210)
  • videos (291-363)
  • users (61-120)
  • comments (394-420)
packages/web-backend/src/Auth.ts (2)
  • provideOptionalAuth (102-114)
  • getCurrentUser (15-36)
apps/web/lib/EffectRuntime.ts (1)
  • EffectRuntime (20-20)
packages/web-backend/src/Videos/VideosPolicy.ts (1)
  • VideosPolicy (9-96)
apps/web/app/(org)/dashboard/dashboard-data.ts (2)
  • OrganizationSettings (34-36)
  • getDashboardData (49-391)
apps/web/lib/Notification.ts (1)
  • createNotification (33-219)
apps/web/utils/effect.ts (1)
  • optionFromTOrFirst (3-4)
apps/web/lib/transcribe.ts (1)
  • transcribeVideo (17-229)
packages/web-backend/src/Database.ts (1)
  • Database (7-17)
apps/web/actions/videos/get-analytics.ts (1)
  • getVideoAnalytics (34-96)
apps/web/app/s/[videoId]/_components/ShareHeader.tsx (1)
  • ShareHeader (24-318)
apps/web/app/s/[videoId]/Share.tsx (1)
  • Share (233-504)
packages/web-backend/src/Videos/VideosRpcs.ts (2)
packages/web-domain/src/Video.ts (3)
  • Video (16-59)
  • NotFoundError (165-169)
  • VerifyVideoPasswordError (133-139)
packages/web-domain/src/Policy.ts (2)
  • Policy (8-12)
  • PolicyDeniedError (20-24)
apps/web/app/(org)/dashboard/caps/Caps.tsx (5)
packages/web-domain/src/Video.ts (3)
  • Video (16-59)
  • VideoId (12-12)
  • VideoId (13-13)
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (1)
  • FolderDataType (18-25)
apps/web/lib/Queries/Analytics.ts (1)
  • useVideosAnalyticsQuery (10-70)
apps/web/lib/EffectRuntime.ts (2)
  • useRpcClient (25-25)
  • useEffectMutation (23-23)
apps/web/app/(org)/dashboard/caps/UploadingContext.tsx (1)
  • useUploadingStatus (50-62)
🪛 GitHub Actions: CI
apps/web/app/s/[videoId]/page.tsx

[error] 1-1: File content differs from formatting output. Re-run the formatter to fix code style.

packages/web-backend/src/Videos/VideosRpcs.ts

[error] 1-1: File content differs from formatting output. Re-run the formatter to fix code style.

apps/web/app/(org)/dashboard/caps/Caps.tsx

[error] 1-1: File content differs from formatting output. Re-run the formatter to fix code style.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (8)
packages/web-backend/src/Videos/VideosRpcs.ts (1)

1-1: LGTM! Policy import supports the analytics error handling.

The Policy import is correctly added to support PolicyDeniedError handling in the VideosGetAnalytics method.

apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)

62-68: Previous critical issues resolved - excellent implementation!

The renamed matchChildren parameter and updated path-matching logic successfully address the critical issues raised in previous reviews:

  1. Improved naming: matchChildren clearly expresses intent—when true, the nav item remains active for child routes.
  2. Fixed matching logic: The segment-aware approach (pathname === path || pathname.startsWith(\${path}/`)) prevents multiple nav items from being active simultaneously, which previously broke the layoutId="navlinks"` animation.

The new Analytics navigation item correctly uses matchChildren: true, which is appropriate for a parent route with nested views.

Also applies to: 94-100

apps/web/app/(org)/dashboard/caps/Caps.tsx (5)

55-85: LGTM! Analytics integration is clean.

The prop rename from dubApiKeyEnabled to analyticsEnabled aligns with the PR's goal of replacing DUB analytics. The analytics query is correctly gated by the flag, and the loading state computation is appropriate.


103-159: LGTM! Comprehensive batch deletion logic.

The mutation handles all scenarios well: full success, partial success, and complete failure. The concurrency limit of 10 is reasonable, and the toast notifications provide clear feedback.


161-170: LGTM!

Single deletion mutation is clean and follows the same pattern as the batch deletion.


279-300: LGTM! Analytics properly wired to CapCard.

The per-video analytics lookup, fallback to 0, and loading state handling are all implemented correctly. The delete logic appropriately chooses between batch and single deletion based on selection state.


309-314: LGTM!

The SelectedCapsBar integration correctly combines both deletion states and uses the batch deletion mutation.

apps/web/app/s/[videoId]/page.tsx (1)

371-381: This review comment is incorrect.

The server-side createNotification() and client-side trackVideoView() are separate systems serving different purposes:

  • Server-side notification (page.tsx line 371): Stores a persistent record in the database for the video owner to see who has viewed their video.
  • Client-side analytics (Share.tsx line 278): Sends telemetry data to /api/analytics/track for viewing patterns and metrics.

Both use the same condition (skip for video owners), but they target different endpoints and systems. This is intentional, not duplicate tracking. There is no double-counting issue.

Likely an incorrect or invalid review comment.

Comment on lines 99 to 101
const rpc = useRpcClient() as {
VideoDelete: (id: Video.VideoId) => Effect.Effect<void, unknown, never>;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Avoid manual type assertion; use proper RPC client typing.

The manual as cast bypasses TypeScript's type safety. If the actual VideoDelete signature differs, this could lead to runtime errors.

Remove the type assertion and rely on the inferred type from useRpcClient():

-  const rpc = useRpcClient() as {
-    VideoDelete: (id: Video.VideoId) => Effect.Effect<void, unknown, never>;
-  };
+  const rpc = useRpcClient();

Run the following script to verify the RPC client's actual type definition:


🏁 Script executed:

#!/bin/bash
# Find the VideoDelete RPC method definition
rg -n "VideoDelete" --type=ts -A 5 -B 2

Length of output: 5807




🏁 Script executed:

#!/bin/bash
# Find useRpcClient definition
rg -n "useRpcClient" --type=ts -B 3 -A 10 | grep -A 10 "function\|const useRpcClient\|export.*useRpcClient" | head -40

Length of output: 3344


🏁 Script executed:

#!/bin/bash
# Check RPC client typing/schema
fd -e ts -e tsx | xargs grep -l "useRpcClient\|RpcClient" | head -5

Length of output: 311


🏁 Script executed:

#!/bin/bash
# Look for RPC client types and their effect signatures
rg -n "Effect\.Effect.*VideoDelete\|VideoDelete.*Effect\.Effect" --type=ts -B 2 -A 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where VideoRpcs is typed/exported
rg -n "VideoRpcs\|export.*Video.*Rpc" --type=ts -B 2 -A 5 packages/web-domain/src/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the actual Rpc.make implementation to understand its return type
rg -n "class Rpc\|function.*Rpc\.make\|Rpc\.make.*=>" --type=ts -A 8 | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check EffectRuntime.ts to see what runSync returns and what Rpc is
cat apps/web/lib/EffectRuntime.ts

Length of output: 846


🏁 Script executed:

#!/bin/bash
# Find Rpc import/definition in EffectRuntime.ts context
rg -n "import.*Rpc\|from.*rpc\|Rpc.*=" apps/web/lib/EffectRuntime.ts -B 5 -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's a type definition for the RPC client or similar
rg -n "RpcClient|RpcType" apps/web/lib/ --type=ts

Length of output: 658


🏁 Script executed:

#!/bin/bash
# Verify other usages of rpc in Caps.tsx to ensure type safety without cast
rg -n "rpc\." apps/web/app/\(org\)/dashboard/caps/Caps.tsx -B 1 -A 1

Length of output: 259


🏁 Script executed:

#!/bin/bash
# Check the imports in Caps.tsx to understand Video type and Rpc setup
head -40 apps/web/app/\(org\)/dashboard/caps/Caps.tsx

Length of output: 1428


🏁 Script executed:

[skip_cloning]

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Check Rpcs.ts to see what type is exported as Rpc
cat apps/web/lib/Rpcs.ts

Length of output: 592


🏁 Script executed:

#!/bin/bash
# Check if VideoDelete error type is truly `never` or has errors
rg -n "VideoDelete\|error:" packages/web-domain/src/Video.ts -B 2 -A 2

Length of output: 0


🏁 Script executed:

[skip_cloning]

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Get the full VideoDelete RPC definition
sed -n '170,180p' packages/web-domain/src/Video.ts

Length of output: 390


🏁 Script executed:

#!/bin/bash
# Check if Rpcs includes VideoRpcs and confirm the structure
rg -n "class Rpcs\|VideoRpcs\|export" packages/web-domain/src/index.ts -A 3 -B 1 | head -50

Length of output: 0


🏁 Script executed:

[skip_cloning]

Length of output: 52


Remove the unsafe type assertion; TypeScript already infers the correct RPC type.

The manual as cast claims an error type of never, but the actual VideoDelete RPC defines errors as Schema.Union(NotFoundError, InternalError, PolicyDeniedError). This type mismatch bypasses type safety. Other files in the codebase (CustomDomainPage.tsx, FolderVideosSection.tsx, useWebRecorder.ts) successfully use useRpcClient() without type assertions, proving it's unnecessary and the type is already correctly inferred by TypeScript.

-  const rpc = useRpcClient() as {
-    VideoDelete: (id: Video.VideoId) => Effect.Effect<void, unknown, never>;
-  };
+  const rpc = useRpcClient();
📝 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 rpc = useRpcClient() as {
VideoDelete: (id: Video.VideoId) => Effect.Effect<void, unknown, never>;
};
const rpc = useRpcClient();
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/Caps.tsx around lines 99 to 101, remove the
unsafe "as" type assertion on useRpcClient() which forces VideoDelete to return
never for errors; TypeScript already infers the correct RPC type (including
Schema.Union error types). Simply call const rpc = useRpcClient(); and use
rpc.VideoDelete(...) without casting so the real error union types
(NotFoundError, InternalError, PolicyDeniedError) are preserved by the compiler.

Comment on lines 172 to 212
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && selectedCaps.length > 0) {
setSelectedCaps([]);
}

return newSelection;
});
};
if (
(e.key === "Delete" || e.key === "Backspace") &&
selectedCaps.length > 0
) {
if (e.key === "Backspace") {
e.preventDefault();
}

const rpc = useRpcClient();
if (
!["INPUT", "TEXTAREA", "SELECT"].includes(
document.activeElement?.tagName || ""
)
) {
deleteCaps(selectedCaps);
}
}

const { mutate: deleteCaps, isPending: isDeletingCaps } = useEffectMutation({
mutationFn: Effect.fn(function* (ids: Video.VideoId[]) {
if (ids.length === 0) return;
if (e.key === "a" && (e.ctrlKey || e.metaKey) && data.length > 0) {
if (
!["INPUT", "TEXTAREA", "SELECT"].includes(
document.activeElement?.tagName || ""
)
) {
e.preventDefault();
setSelectedCaps(data.map((cap) => cap.id));
}
}
};

const fiber = yield* Effect.gen(function* () {
const results = yield* Effect.all(
ids.map((id) => rpc.VideoDelete(id).pipe(Effect.exit)),
{ concurrency: 10 },
);
window.addEventListener("keydown", handleKeyDown);

const successCount = results.filter(Exit.isSuccess).length;
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [selectedCaps, data, deleteCaps]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove mutation function from useEffect dependencies.

The deleteCaps mutation function is stable and doesn't need to be in the dependency array. Including it is unnecessary and deviates from React best practices.

     return () => {
       window.removeEventListener("keydown", handleKeyDown);
     };
-  }, [selectedCaps, data, deleteCaps]);
+  }, [selectedCaps, data]);
📝 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
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && selectedCaps.length > 0) {
setSelectedCaps([]);
}
return newSelection;
});
};
if (
(e.key === "Delete" || e.key === "Backspace") &&
selectedCaps.length > 0
) {
if (e.key === "Backspace") {
e.preventDefault();
}
const rpc = useRpcClient();
if (
!["INPUT", "TEXTAREA", "SELECT"].includes(
document.activeElement?.tagName || ""
)
) {
deleteCaps(selectedCaps);
}
}
const { mutate: deleteCaps, isPending: isDeletingCaps } = useEffectMutation({
mutationFn: Effect.fn(function* (ids: Video.VideoId[]) {
if (ids.length === 0) return;
if (e.key === "a" && (e.ctrlKey || e.metaKey) && data.length > 0) {
if (
!["INPUT", "TEXTAREA", "SELECT"].includes(
document.activeElement?.tagName || ""
)
) {
e.preventDefault();
setSelectedCaps(data.map((cap) => cap.id));
}
}
};
const fiber = yield* Effect.gen(function* () {
const results = yield* Effect.all(
ids.map((id) => rpc.VideoDelete(id).pipe(Effect.exit)),
{ concurrency: 10 },
);
window.addEventListener("keydown", handleKeyDown);
const successCount = results.filter(Exit.isSuccess).length;
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [selectedCaps, data, deleteCaps]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && selectedCaps.length > 0) {
setSelectedCaps([]);
}
if (
(e.key === "Delete" || e.key === "Backspace") &&
selectedCaps.length > 0
) {
if (e.key === "Backspace") {
e.preventDefault();
}
if (
!["INPUT", "TEXTAREA", "SELECT"].includes(
document.activeElement?.tagName || ""
)
) {
deleteCaps(selectedCaps);
}
}
if (e.key === "a" && (e.ctrlKey || e.metaKey) && data.length > 0) {
if (
!["INPUT", "TEXTAREA", "SELECT"].includes(
document.activeElement?.tagName || ""
)
) {
e.preventDefault();
setSelectedCaps(data.map((cap) => cap.id));
}
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [selectedCaps, data]);
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/Caps.tsx around lines 172-212 the useEffect
dependency array includes the stable mutation function deleteCaps unnecessarily;
remove deleteCaps from the dependency array so it becomes [selectedCaps, data],
and if your linter complains add a single-line exemption (//
eslint-disable-next-line react-hooks/exhaustive-deps) immediately above the
useEffect declaration or alternatively memoize deleteCaps with useCallback where
it's defined and keep the linter happy.

Comment on lines 624 to 640
let toplLevelCommentId = Option.none<Comment.CommentId>();

if (Option.isSome(replyId)) {
const [parentComment] = yield* db.use((db) =>
db
.select({ parentCommentId: comments.parentCommentId })
.from(comments)
.where(eq(comments.id, replyId.value))
.limit(1)
);
toplLevelCommentId = Option.fromNullable(parentComment?.parentCommentId);
}

const commentToBringToTheTop = Option.orElse(
toplLevelCommentId,
() => commentId
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in variable name.

The variable toplLevelCommentId should be topLevelCommentId (missing the second 'e').

Apply this diff:

-    let toplLevelCommentId = Option.none<Comment.CommentId>();
+    let topLevelCommentId = Option.none<Comment.CommentId>();
 
     if (Option.isSome(replyId)) {
       const [parentComment] = yield* db.use((db) =>
@@ -631,11 +631,11 @@
           .where(eq(comments.id, replyId.value))
           .limit(1)
       );
-      toplLevelCommentId = Option.fromNullable(parentComment?.parentCommentId);
+      topLevelCommentId = Option.fromNullable(parentComment?.parentCommentId);
     }
 
     const commentToBringToTheTop = Option.orElse(
-      toplLevelCommentId,
+      topLevelCommentId,
       () => commentId
     );
📝 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
let toplLevelCommentId = Option.none<Comment.CommentId>();
if (Option.isSome(replyId)) {
const [parentComment] = yield* db.use((db) =>
db
.select({ parentCommentId: comments.parentCommentId })
.from(comments)
.where(eq(comments.id, replyId.value))
.limit(1)
);
toplLevelCommentId = Option.fromNullable(parentComment?.parentCommentId);
}
const commentToBringToTheTop = Option.orElse(
toplLevelCommentId,
() => commentId
);
let topLevelCommentId = Option.none<Comment.CommentId>();
if (Option.isSome(replyId)) {
const [parentComment] = yield* db.use((db) =>
db
.select({ parentCommentId: comments.parentCommentId })
.from(comments)
.where(eq(comments.id, replyId.value))
.limit(1)
);
topLevelCommentId = Option.fromNullable(parentComment?.parentCommentId);
}
const commentToBringToTheTop = Option.orElse(
topLevelCommentId,
() => commentId
);
🤖 Prompt for AI Agents
In apps/web/app/s/[videoId]/page.tsx around lines 624 to 640, there's a typo:
the variable is named "toplLevelCommentId" but should be "topLevelCommentId";
rename the variable declaration and all subsequent references (including the
Option.fromNullable assignment and the Option.orElse call) to topLevelCommentId
to fix the spelling and ensure consistent usage.

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: 1

♻️ Duplicate comments (4)
apps/web/app/api/analytics/track/route.ts (2)

47-88: Validate occurredAt before calling toISOString().

If a client passes an invalid occurredAt, new Date(body.occurredAt) becomes Invalid Date, and calling toISOString() on line 88 will throw a RangeError, returning a 500 instead of a proper 400 validation error.

Apply this diff to add validation:

 const timestamp = body.occurredAt ? new Date(body.occurredAt) : new Date();
+
+if (Number.isNaN(timestamp.getTime())) {
+  return Response.json(
+    { error: "occurredAt must be a valid ISO timestamp" },
+    { status: 400 }
+  );
+}

25-108: Rebuild the route using the required HttpApi builder.

Per the established API conventions, this handler must be expressed via @effect/platform's HttpApi builder and exported through apiToHandler rather than invoking runPromise directly. Please restructure the route to align with the shared API pattern.

Based on learnings

apps/web/app/(org)/dashboard/caps/Caps.tsx (2)

172-212: Remove stable mutation function from dependencies.

The deleteCaps mutation function is stable and doesn't need to be in the dependency array. This deviates from React best practices.

     return () => {
       window.removeEventListener("keydown", handleKeyDown);
     };
-  }, [selectedCaps, data, deleteCaps]);
+  }, [selectedCaps, data]);

99-101: Remove the unsafe type assertion.

The manual as cast bypasses TypeScript's type safety. The actual VideoDelete RPC defines errors as Schema.Union(NotFoundError, InternalError, PolicyDeniedError), not never. Simply use:

-  const rpc = useRpcClient() as {
-    VideoDelete: (id: Video.VideoId) => Effect.Effect<void, unknown, never>;
-  };
+  const rpc = useRpcClient();
🧹 Nitpick comments (5)
scripts/analytics/tinybird/pipes/analytics_sessions_mv_pipe.pipe (2)

2-3: Consider updating the description to include "os".

The description mentions "browser/device metadata" but the query also materializes os (line 11). For completeness, consider updating to "browser/device/os metadata".


7-7: Prefer deterministic timestamp aggregation for the date field.

Using anyLast(timestamp) picks an arbitrary timestamp from the session, making the date field non-deterministic. Consider using min(timestamp) for the session start date or max(timestamp) for the session end date to ensure consistent, meaningful results.

Apply this diff to use the session start date:

-		toDate(anyLast(timestamp)) AS date,
+		toDate(min(timestamp)) AS date,
apps/web/app/api/analytics/track/route.ts (1)

84-84: Remove unused @ts-expect-error directive.

Static analysis indicates this directive is no longer needed, suggesting the type issue has been resolved. Please remove it for cleaner code.

Apply this diff:

-// @ts-expect-error - Tinybird service can be yielded directly
 const tinybird = yield* Tinybird;
apps/web/app/(org)/dashboard/caps/Caps.tsx (1)

103-159: Consider dismissing the loading toast explicitly.

The toast.loading() call in onMutate is not explicitly dismissed. While subsequent toast.success() or toast.error() calls will display, the loading toast may linger. Consider using toast.promise() for automatic lifecycle management or storing the toast ID to dismiss it.

Example with toast.promise():

const { mutate: deleteCaps, isPending: isDeletingCaps } = useEffectMutation({
  mutationFn: Effect.fn(function* (ids: Video.VideoId[]) {
    // ... existing logic
  }),
  onMutate: (ids: Video.VideoId[]) => {
    // toast.promise handles loading/success/error automatically
  },
  onSuccess: (data) => {
    setSelectedCaps([]);
    router.refresh();
  },
  // Consider wrapping the mutation call with toast.promise where it's invoked
});

Alternatively, store and dismiss the toast ID:

let toastId: string | number;
onMutate: (ids: Video.VideoId[]) => {
  toastId = toast.loading(`Deleting ${ids.length} cap${ids.length === 1 ? "" : "s"}...`);
},
onSuccess: (data) => {
  toast.dismiss(toastId);
  // ... rest of success logic
},
onError: (error) => {
  toast.dismiss(toastId);
  // ... rest of error logic
},
apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx (1)

82-98: Consider using consistent fallback for topCaps.

Line 89 sets topCaps to null when capId is present, while line 97 uses an empty array [] as the fallback. Both prevent rendering in OtherStats, but consistency would improve code clarity.

 		: {
 				countries: [],
 				cities: [],
 				browsers: [],
 				operatingSystems: [],
 				deviceTypes: [],
-				topCaps: [],
+				topCaps: null,
 			};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c321959 and 7090526.

⛔ Files ignored due to path filters (1)
  • apps/web/public/logos/browsers/maxthon.svg is excluded by !**/*.svg
📒 Files selected for processing (8)
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (8 hunks)
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/caps/Caps.tsx (5 hunks)
  • apps/web/app/api/analytics/track/route.ts (1 hunks)
  • apps/web/app/s/[videoId]/Share.tsx (5 hunks)
  • apps/web/app/s/[videoId]/page.tsx (6 hunks)
  • packages/web-backend/src/Videos/VideosRpcs.ts (2 hunks)
  • scripts/analytics/tinybird/pipes/analytics_sessions_mv_pipe.pipe (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/web-backend/src/Videos/VideosRpcs.ts
  • apps/web/app/s/[videoId]/page.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Files:

  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

On the client, always use useEffectQuery or useEffectMutation from @/lib/EffectRuntime; never call EffectRuntime.run* directly in components.

Files:

  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
🧠 Learnings (5)
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/**/*.{ts,tsx,js,jsx} : On the client, always use `useEffectQuery` or `useEffectMutation` from `@/lib/EffectRuntime`; never call `EffectRuntime.run*` directly in components.

Applied to files:

  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
  • apps/web/app/(org)/dashboard/caps/Caps.tsx
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : API routes in Next.js (`apps/web/app/api/*`) must use `effect/platform`'s `HttpApi` builder and follow the existing class/group/endpoint pattern; avoid ad-hoc handlers.

Applied to files:

  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : Convert effectful APIs to Next.js handlers using `apiToHandler(ApiLive)` and export only the returned `handler`; avoid calling `runPromise` directly in route files.

Applied to files:

  • apps/web/app/api/analytics/track/route.ts
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to apps/web/app/api/* : On the server, always run effects through `EffectRuntime.runPromise` after `provideOptionalAuth` to ensure cookies and per-request context are attached.

Applied to files:

  • apps/web/app/api/analytics/track/route.ts
  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
📚 Learning: 2025-09-22T14:19:56.010Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T14:19:56.010Z
Learning: Applies to **/queries.ts : Do not edit auto-generated files named `queries.ts`.

Applied to files:

  • apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
🧬 Code graph analysis (4)
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (1)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/index.ts (1)
  • ChartLineIcon (27-27)
apps/web/app/api/analytics/track/route.ts (3)
apps/web/lib/server.ts (1)
  • runPromise (141-153)
packages/web-backend/src/Tinybird/index.ts (1)
  • Tinybird (28-276)
packages/web-backend/src/Auth.ts (1)
  • provideOptionalAuth (102-114)
apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx (5)
apps/web/app/(org)/dashboard/analytics/types.ts (2)
  • AnalyticsRange (1-1)
  • OrgAnalyticsResponse (10-33)
apps/web/app/(org)/dashboard/Contexts.tsx (1)
  • useDashboardContext (50-50)
apps/web/lib/EffectRuntime.ts (1)
  • useEffectQuery (22-22)
apps/web/app/(org)/dashboard/analytics/components/Header.tsx (1)
  • Header (90-429)
apps/web/app/(org)/dashboard/analytics/components/OtherStats.tsx (1)
  • OtherStats (122-202)
apps/web/app/(org)/dashboard/caps/Caps.tsx (5)
apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx (1)
  • VideoData (37-50)
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (1)
  • FolderDataType (18-25)
packages/web-domain/src/Video.ts (3)
  • Video (16-59)
  • VideoId (12-12)
  • VideoId (13-13)
apps/web/lib/EffectRuntime.ts (2)
  • useRpcClient (25-25)
  • useEffectMutation (23-23)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)
  • CapCard (101-648)
🪛 GitHub Check: Typecheck
apps/web/app/api/analytics/track/route.ts

[failure] 84-84:
Unused '@ts-expect-error' directive.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (16)
scripts/analytics/tinybird/pipes/analytics_sessions_mv_pipe.pipe (2)

5-14: Previous critical issue resolved.

The query now correctly groups by tenant_id and session_id (line 14), ensuring exactly one row per session is materialized. This addresses the duplicate session issue from the previous review.


16-17: LGTM!

The materialized view configuration is correct.

apps/web/app/s/[videoId]/Share.tsx (3)

38-63: LGTM! Session management is well-implemented.

The session ID generation and persistence logic correctly handles:

  • Non-browser environments
  • Expiry checking with 30-minute TTL
  • Crypto API availability with a safe fallback
  • Error recovery with "anonymous" default

65-132: LGTM! Analytics tracking correctly implemented.

The beacon-first approach with fetch fallback is appropriate. The previous issue with aborting the keepalive request has been correctly resolved by removing the AbortController.


273-283: LGTM! View tracking with proper privacy guard.

The effect correctly skips tracking when the viewer is the video owner, preventing self-inflation of analytics. Dependencies are complete.

apps/web/app/(org)/dashboard/caps/Caps.tsx (5)

58-64: LGTM! Clean prop rename.

The transition from dubApiKeyEnabled to analyticsEnabled is consistent and aligns with the Tinybird analytics migration.


79-85: LGTM! Analytics query integration is correct.

The query setup properly gates analytics fetching with analyticsEnabled and correctly computes the loading state.


87-97: LGTM! Selection toggle logic is sound.

The toggle logic correctly adds/removes caps from selection and tracks the previous count appropriately.


161-170: LGTM! Single cap deletion is correctly implemented.

The mutation uses useEffectMutation properly and provides appropriate user feedback.


280-294: LGTM! CapCard integration with analytics is correct.

Per-video analytics are properly extracted and wired, and the delete logic appropriately handles both single and batch deletion scenarios.

apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx (1)

56-56: Verify that disabling browser cache is intentional.

The cache: "no-store" option prevents browser caching, ensuring fresh analytics data but potentially increasing server load. While React Query's staleTime (60s) provides some caching, every navigation away and back will trigger a new fetch.

If this is intentional for data freshness, consider documenting the decision. If not, removing cache: "no-store" would allow browser caching and reduce unnecessary requests:

 				const response = yield* Effect.tryPromise({
-					try: () => fetch(url.toString(), { cache: "no-store" }),
+					try: () => fetch(url.toString()),
 					catch: (cause: unknown) => cause as Error,
 				});
apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx (5)

39-39: LGTM - ChartLineIcon import is correct.

The import of ChartLineIcon from AnimatedIcons is properly structured and aligns with the new Analytics navigation item.


62-68: LGTM - Analytics navigation item properly configured.

The new Analytics nav item is well-structured with appropriate matchChildren: true to handle analytics sub-routes.


94-100: Excellent improvements to path matching logic.

The refactored isPathActive function successfully addresses the previous review concerns:

  1. Clear naming: matchChildren accurately describes the prefix-matching behavior (not query param handling).
  2. Correct logic: Exact matching by default prevents multiple active items; optional prefix matching with pathname.startsWith(\${path}/`)` ensures proper segment boundaries.

The implementation correctly prevents /dashboard/caps from matching /dashboard/capsule while allowing intentional parent-route matching when matchChildren: true.


290-290: LGTM - matchChildren properly propagated through component hierarchy.

The matchChildren flag is correctly threaded from the navigation configuration through AdminNavItems rendering (line 290) to the NavItem component props (lines 319, 409, 423) and final usage in path matching (line 444). Type signatures (line 421) and default value handling (?? false) are consistent throughout.

Also applies to: 319-319, 409-409, 421-421, 423-423, 444-444


56-61: The review comment is based on an incorrect assumption about route structure.

The codebase has no dynamic cap routes (like /dashboard/caps/[id] or /dashboard/caps/edit/:id). The only routes under /dashboard/caps are:

  • /dashboard/caps (main caps list)
  • /dashboard/caps/record (record page, which has its own separate nav item)

Since no child routes exist that should keep "My Caps" highlighted, the absence of matchChildren is correct. The exact pathname matching works properly for the actual route structure.

Likely an incorrect or invalid review comment.

Comment on lines +41 to +69
const query = useEffectQuery({
queryKey: ["dashboard-analytics", orgId, selectedSpaceId, range, capId],
queryFn: () =>
Effect.gen(function* () {
if (!orgId) return null;
const url = new URL("/api/dashboard/analytics", window.location.origin);
url.searchParams.set("orgId", orgId);
url.searchParams.set("range", range);
if (selectedSpaceId) {
url.searchParams.set("spaceId", selectedSpaceId);
}
if (capId) {
url.searchParams.set("capId", capId);
}
const response = yield* Effect.tryPromise({
try: () => fetch(url.toString(), { cache: "no-store" }),
catch: (cause: unknown) => cause as Error,
});
if (!response.ok) {
return yield* Effect.fail(new Error("Failed to load analytics"));
}
return yield* Effect.tryPromise({
try: () => response.json() as Promise<{ data: OrgAnalyticsResponse }>,
catch: (cause: unknown) => cause as Error,
});
}),
enabled: Boolean(orgId),
staleTime: 60 * 1000,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider displaying error state when analytics fetch fails.

The query can fail (line 60), but query.error is never displayed to users. When the analytics API is unavailable, users see only a loading state or stale data without any indication of failure.

Add error handling after line 73:

 	const analytics = (query.data as { data: OrgAnalyticsResponse } | undefined)
 		?.data;
+
+	if (query.error) {
+		return (
+			<div className="rounded-xl border border-red-5 bg-red-2 p-6 text-red-11">
+				Failed to load analytics. Please try again later.
+			</div>
+		);
+	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/analytics/components/AnalyticsDashboard.tsx
around lines 41 to 69, the data fetch can fail but the component never surfaces
query.error to the UI; after the query is defined (around line 73) add handling
that checks query.isError or query.error and renders a visible error state
(e.g., an error message component or fallback UI) returning early from the
component so users see the failure instead of a perpetual loading/stale view;
include the error message text (or a user-friendly message) and optionally a
retry action that calls query.refetch.

@richiemcilroy richiemcilroy merged commit fe38ff4 into main Nov 13, 2025
15 of 16 checks passed
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.

3 participants