Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Dec 1, 2025

Summary by CodeRabbit

Release Notes

  • New Features
    • Added folder-level analytics dashboards with public sharing capabilities
    • Enabled conversation tracking in analytics dashboards (based on your plan features)
    • Analytics can now be filtered and organized by folders for better data management

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Contributor

vercel bot commented Dec 1, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Dec 3, 2025 2:23am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 1, 2025

Warning

Rate limit exceeded

@steven-tey has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 19 minutes and 18 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between ea6e78b and 579d807.

📒 Files selected for processing (2)
  • apps/web/app/api/og/analytics/route.tsx (4 hunks)
  • apps/web/ui/folders/folder-actions.tsx (7 hunks)

Walkthrough

This PR extends analytics dashboards to support folder-based access alongside existing link-based dashboards. It introduces folder routing in dashboard API endpoints, adds folder-specific permissions validation, updates Prisma schema relations, and expands UI components (modals, filters, toggles) to accommodate both link and folder contexts.

Changes

Cohort / File(s) Summary
Database Schema Updates
packages/prisma/schema/dashboard.prisma, packages/prisma/schema/folder.prisma
Added optional folderId and folder relation to Dashboard; added corresponding dashboard relation to Folder with cascade constraints.
Zod Schemas & Validation
apps/web/lib/zod/schemas/dashboard.ts
Extended dashboardSchema with folderId (nullable string) and showConversions (boolean) fields; introduced createDashboardQuerySchema supporting either domain/key or folderId creation paths; updated updateDashboardBodySchema to include showConversions.
Analytics API Routes
apps/web/app/api/analytics/dashboard/route.ts, apps/web/app/api/og/analytics/route.tsx
Added folder-based query parameter routing; implemented dual-branch logic (folder vs. link) with respective data fetching, validation, and error handling; extended rate-limiting and analytics payload with folder/link/workspace IDs; updated OG route to support linkId/folderId selection and Tinybird v3 endpoint.
Dashboard Management API
apps/web/app/api/dashboards/route.ts
Replaced domain/key parsing with generalized schema; added dual-path dashboard creation (link-based or folder-based) with plan capability checks; wired canTrackConversions into dashboard creation; added folder access verification and backwards-compatibility updates for publicStats.
Folder Dashboard Endpoint
apps/web/app/api/folders/[folderId]/dashboard/route.ts
New GET route to fetch a folder's dashboard; includes workspace wrapping, permission enforcement, and dashboard validation.
Data Fetchers & SWR Hooks
apps/web/lib/fetchers/get-dashboard.ts, apps/web/lib/swr/use-folder-link-count.ts, apps/web/lib/swr/use-folder.ts
Expanded dashboard fetcher to include nested link.id, folder.id, and folder.name; added optional enabled flag to useFolderLinkCount; exposed error from useFolder hook.
Analytics Provider & Core Hooks
apps/web/ui/analytics/analytics-provider.tsx, apps/web/ui/analytics/use-analytics-query.tsx
Refactored AnalyticsDashboardProps to support both link-variant (domain/key/url) and folder-variant (folderId/folderName); added folderId to AnalyticsContext; extended useAnalyticsQuery with optional defaultFolderId parameter for fallback resolution.
Analytics UI Components
apps/web/ui/analytics/index.tsx, apps/web/ui/analytics/toggle.tsx, apps/web/ui/analytics/use-analytics-filters.tsx, apps/web/ui/analytics/top-links.tsx, apps/web/ui/analytics/share-button.tsx, apps/web/ui/analytics/partner-section.tsx
Updated conditional rendering to accommodate folder-only dashboards; added folder icon and name display in toggle; extracted DomainFilterItem constant for reuse; adjusted TopLinks visibility condition; extended share button with folder write-permission checks and folderId-based modal initialization; removed unused Globe import.
Share Dashboard Modal
apps/web/ui/modals/share-dashboard-modal.tsx
Added folder-aware data fetching and rendering; introduced FolderCard component alongside LinkCard; updated useShareDashboardModal signature to accept ShareDashboardModalInnerProps supporting both link and folder paths; added controlled component handling for conversion analytics and index toggles.
Folder UI Components
apps/web/ui/folders/folder-icon.tsx, apps/web/ui/folders/folder-actions.tsx, apps/web/ui/folders/simple-folder-card.tsx, apps/web/ui/modals/set-default-folder-modal.tsx
Made folder prop optional in FolderIcon; extended folder actions with ShareDashboardModal integration, keyboard shortcut ('s') for share, and new "Share Analytics" / "Copy Folder ID" actions; introduced new SimpleFolderCard component for horizontal folder display; updated SetDefaultFolderModal to use SimpleFolderCard with enhanced layout and Cancel action.
Share Page
apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx
Extended generateMetadata to handle both link and folder data, returning Promise<Metadata>; broadened existence checks to account for folder presence; updated DashboardPage rendering to conditionally supply link-based or folder-based props to Analytics component.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • Dual-branch logic in API routes (analytics/dashboard/route.ts, dashboards/route.ts): folder vs. link path divergence with distinct validation, error messages, and rate-limiting requires careful verification of both branches.
  • Prisma schema cascade relations: Verify that cascade delete/update behavior on Dashboard↔Folder relation does not conflict with existing link relations or create unintended cascades.
  • Analytics context and props type union: Review the union type for AnalyticsDashboardProps to ensure correct mutual exclusivity and never constraints between (domain/key/url) and (folderId/folderName) variants.
  • Share modal refactor: useShareDashboardModal signature change and folder-aware fetching/rendering logic warrants testing both link and folder creation paths.
  • Permission checks: Verify verifyFolderAccess and canUpdateFolder checks are consistently applied across new folder action (share) and existing actions.

Possibly related PRs

  • Improve folders linkCount #2452: Modifies useFolderLinkCount hook and folder link-count design; overlaps with changes to use-folder-link-count.ts in this PR.
  • Program Analytics page #2519: Updates analytics stack (useAnalyticsQuery, analytics-provider, Tinybird response handling); shares code-level changes to analytics query and context propagation.
  • Refactor to @axiomhq/nextjs #3069: Modifies date-range validation logic in analytics/dashboard/route.ts; overlaps with validation flow adjustments in this PR.

Suggested reviewers

  • devkiran

🐰 Hop, hop, dashboard dreams take flight,
Folders bloom where links once shone so bright,
Analytics dance through folder gates with glee,
Share buttons hop, permissions dance free,
Two paths converge—oh what a sight! ✨📊

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 'Public folder analytics dashboards' accurately captures the primary change: enabling public analytics dashboards for folders alongside existing link-based dashboards.

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.

@TWilson023 TWilson023 marked this pull request as ready for review December 2, 2025 16:28
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: 3

Caution

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

⚠️ Outside diff range comments (1)
apps/web/app/api/analytics/dashboard/route.ts (1)

125-143: Potential TypeError: link may be undefined in folder-only requests.

When a folder dashboard is accessed without domain and key parameters, link remains undefined (see line 63 - it's only set when the folder has links AND domain/key are provided). The rate limiting code at line 135 unconditionally accesses link.id, which will throw a TypeError in production.

Apply this diff to handle folder-only requests:

     // Rate limit in production
     if (process.env.NODE_ENV !== "development") {
       const ip = ipAddress(req);
+      const rateLimitKey = link?.id ?? folder?.id;
+      if (!rateLimitKey) {
+        throw new DubApiError({
+          code: "bad_request",
+          message: "Unable to determine rate limit context",
+        });
+      }
       // for demo links, we rate limit at:
       // - 15 requests per 10 seconds if groupBy is "count"
       // - 15 request per minute if groupBy is not "count"
       // for non-demo links, we rate limit at 10 requests per 10 seconds
       const { success } = await ratelimit(
         demoLink ? 15 : 10,
         !demoLink || groupBy === "count" ? "10 s" : "1 m",
-      ).limit(`analytics-dashboard:${link.id}:${ip}:${groupBy}`);
+      ).limit(`analytics-dashboard:${rateLimitKey}:${ip}:${groupBy}`);
🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx (1)

88-94: Consider handling loading and error states for the share button.

The ShareDashboardButton is only rendered when both folderId and folder exist. Since useFolder can return loading and error states, the button will be hidden during loading or after a fetch error, which could be confusing.

Consider:

  • Showing a loading state/skeleton while folder is loading
  • Handling the error case (perhaps still allowing sharing with just the folderId, or showing an error message)
  const { folderId } = useCurrentFolderId();
- const { folder } = useFolder({ folderId });
+ const { folder, loading, error } = useFolder({ folderId });

  return (
    <>
      <LinkBuilder />
-     {folderId && folder && <ShareDashboardButton folderId={folderId} />}
+     {folderId && !error && <ShareDashboardButton folderId={folderId} loading={loading} />}
      <div className="hidden sm:block">
        <CreateLinkButton className="h-9" />
      </div>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3716b15 and 904f175.

📒 Files selected for processing (20)
  • apps/web/app/api/analytics/dashboard/route.ts (3 hunks)
  • apps/web/app/api/dashboards/route.ts (2 hunks)
  • apps/web/app/api/folders/[folderId]/dashboard/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx (4 hunks)
  • apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx (3 hunks)
  • apps/web/lib/fetchers/get-dashboard.ts (1 hunks)
  • apps/web/lib/swr/use-folder-link-count.ts (1 hunks)
  • apps/web/lib/swr/use-folder.ts (2 hunks)
  • apps/web/lib/zod/schemas/dashboard.ts (1 hunks)
  • apps/web/ui/analytics/analytics-provider.tsx (5 hunks)
  • apps/web/ui/analytics/index.tsx (1 hunks)
  • apps/web/ui/analytics/share-button.tsx (1 hunks)
  • apps/web/ui/analytics/toggle.tsx (2 hunks)
  • apps/web/ui/analytics/use-analytics-filters.tsx (3 hunks)
  • apps/web/ui/analytics/use-analytics-query.tsx (2 hunks)
  • apps/web/ui/folders/folder-actions.tsx (6 hunks)
  • apps/web/ui/folders/folder-icon.tsx (1 hunks)
  • apps/web/ui/modals/share-dashboard-modal.tsx (10 hunks)
  • packages/prisma/schema/dashboard.prisma (1 hunks)
  • packages/prisma/schema/folder.prisma (1 hunks)
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 3136
File: apps/web/ui/layout/sidebar/app-sidebar-nav.tsx:377-379
Timestamp: 2025-11-19T17:26:51.932Z
Learning: In the dub repository, TWilson023 prefers an incremental approach to route updates: fixing the most user-visible issues first (like sidebar navigation) while deferring less visible internal references to follow-up PRs, especially when redirects provide backward compatibility.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-06-16T19:21:23.506Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the `useAnalyticsFilterOption` function in `apps/web/ui/analytics/utils.ts`, the pattern `options?.context ?? useContext(AnalyticsContext)` is intentionally designed as a complete replacement strategy, not a merge. When `options.context` is provided, it should contain all required fields (`baseApiPath`, `queryString`, `selectedTab`, `requiresUpgrade`) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.

Applied to files:

  • apps/web/ui/analytics/use-analytics-query.tsx
  • apps/web/ui/analytics/toggle.tsx
  • apps/web/ui/analytics/use-analytics-filters.tsx
  • apps/web/ui/analytics/analytics-provider.tsx
  • apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx
  • apps/web/ui/modals/share-dashboard-modal.tsx
  • apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx
  • apps/web/ui/folders/folder-actions.tsx
  • apps/web/ui/modals/share-dashboard-modal.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.

Applied to files:

  • apps/web/app/api/analytics/dashboard/route.ts
📚 Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.

Applied to files:

  • apps/web/lib/zod/schemas/dashboard.ts
  • apps/web/app/api/dashboards/route.ts
📚 Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.

Applied to files:

  • apps/web/ui/modals/share-dashboard-modal.tsx
🧬 Code graph analysis (15)
apps/web/ui/analytics/use-analytics-query.tsx (2)
apps/web/lib/analytics/constants.ts (1)
  • DUB_LINKS_ANALYTICS_INTERVAL (13-13)
apps/web/lib/analytics/types.ts (1)
  • EventType (31-31)
apps/web/app/api/folders/[folderId]/dashboard/route.ts (5)
apps/web/app/api/dashboards/route.ts (1)
  • GET (16-27)
apps/web/lib/auth/workspace.ts (1)
  • withWorkspace (55-488)
apps/web/lib/folder/permissions.ts (1)
  • verifyFolderAccess (19-73)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/zod/schemas/dashboard.ts (1)
  • dashboardSchema (4-15)
apps/web/ui/analytics/toggle.tsx (2)
apps/web/ui/folders/folder-icon.tsx (1)
  • FolderIcon (66-108)
packages/utils/src/constants/misc.ts (1)
  • GOOGLE_FAVICON_URL (26-27)
apps/web/ui/analytics/use-analytics-filters.tsx (1)
packages/utils/src/constants/misc.ts (1)
  • GOOGLE_FAVICON_URL (26-27)
apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx (3)
apps/web/lib/swr/use-folder.ts (1)
  • useFolder (6-42)
apps/web/ui/modals/link-builder/index.tsx (1)
  • LinkBuilder (57-59)
apps/web/ui/modals/share-dashboard-modal.tsx (1)
  • useShareDashboardModal (465-485)
apps/web/lib/swr/use-folder-link-count.ts (2)
apps/web/lib/swr/use-links-count.ts (1)
  • useLinksCount (10-53)
apps/web/lib/types.ts (1)
  • FolderLinkCount (556-559)
apps/web/app/api/analytics/dashboard/route.ts (5)
apps/web/lib/api/errors.ts (1)
  • DubApiError (58-75)
packages/prisma/index.ts (1)
  • prisma (3-9)
packages/utils/src/constants/dub-domains.ts (1)
  • DUB_DEMO_LINKS (157-212)
apps/web/lib/api/utils/assert-valid-date-range-for-plan.ts (1)
  • assertValidDateRangeForPlan (4-21)
apps/web/lib/types.ts (1)
  • PlanProps (206-206)
apps/web/lib/zod/schemas/dashboard.ts (1)
apps/web/lib/zod/schemas/links.ts (1)
  • domainKeySchema (287-303)
apps/web/ui/analytics/index.tsx (1)
apps/web/ui/analytics/top-links.tsx (1)
  • TopLinks (56-259)
apps/web/ui/analytics/share-button.tsx (2)
apps/web/ui/analytics/analytics-provider.tsx (1)
  • AnalyticsContext (56-99)
apps/web/ui/modals/share-dashboard-modal.tsx (1)
  • useShareDashboardModal (465-485)
apps/web/ui/folders/folder-actions.tsx (1)
apps/web/lib/folder/constants.ts (1)
  • unsortedLinks (44-49)
apps/web/ui/modals/share-dashboard-modal.tsx (5)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (7-48)
apps/web/lib/swr/use-folder.ts (1)
  • useFolder (6-42)
apps/web/lib/types.ts (1)
  • Folder (523-523)
apps/web/lib/swr/use-folder-link-count.ts (1)
  • useFolderLinkCount (5-30)
apps/web/ui/folders/folder-icon.tsx (1)
  • FolderIcon (66-108)
apps/web/ui/analytics/analytics-provider.tsx (3)
apps/web/lib/types.ts (1)
  • PlanProps (206-206)
apps/web/ui/analytics/use-analytics-query.tsx (1)
  • useAnalyticsQuery (13-125)
apps/web/lib/analytics/constants.ts (2)
  • DUB_PARTNERS_ANALYTICS_INTERVAL (14-14)
  • DUB_LINKS_ANALYTICS_INTERVAL (13-13)
apps/web/app/api/dashboards/route.ts (6)
apps/web/lib/zod/schemas/dashboard.ts (2)
  • createDashboardQuerySchema (17-21)
  • dashboardSchema (4-15)
apps/web/lib/plan-capabilities.ts (1)
  • getPlanCapabilities (4-22)
apps/web/lib/api/links/get-link-or-throw.ts (1)
  • getLinkOrThrow (21-101)
apps/web/lib/folder/permissions.ts (1)
  • verifyFolderAccess (19-73)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/api/create-id.ts (1)
  • createId (68-73)
apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx (1)
apps/web/lib/fetchers/get-dashboard.ts (1)
  • getDashboard (4-34)
🔇 Additional comments (28)
apps/web/ui/folders/folder-icon.tsx (1)

73-79: LGTM! Clean handling of optional folder prop.

The conditional check folder ? determineFolderIcon(folder) : "write" safely handles the optional prop and provides a sensible default.

apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx (2)

72-84: LGTM! Conditional props spreading is well-structured.

The non-null assertions on data.folder! are safe given the early return at line 38 ensures at least one of link or folder exists when this code is reached.


19-22: Code logic correctly handles dashboard with both link and folder.

The condition !data?.link && !data?.folder appropriately returns early only when both are missing. The Prisma schema allows both linkId and folderId to be set on the same Dashboard, and the code correctly prioritizes link over folder via ternary operators when both exist. The non-null assertions on data.folder are safe because they only execute after the condition passes. No changes needed.

apps/web/ui/folders/folder-actions.tsx (3)

60-61: LGTM! ShareDashboardModal integration.

The hook is correctly initialized with the folder ID.


75-92: LGTM! Keyboard shortcut for sharing analytics.

The "s" shortcut is properly guarded with the same conditions as the UI button (!unsortedLinks && canUpdateFolder).


186-198: LGTM! Share Analytics action.

The button is properly guarded with permission checks and uses appropriate iconography.

apps/web/lib/fetchers/get-dashboard.ts (1)

21-26: LGTM! Clean extension of dashboard query.

The folder selection follows the established pattern and selects only the necessary fields.

apps/web/ui/analytics/index.tsx (1)

67-67: LGTM! Correct visibility logic for folder dashboards.

Changing from !dashboardProps to !dashboardProps?.key ensures TopLinks renders for folder-based dashboards (which have folderId but no key), while still hiding it for link-specific dashboards.

apps/web/ui/analytics/use-analytics-query.tsx (1)

17-17: LGTM! Clean fallback chain for folder context.

The defaultFolderId parameter follows the established pattern used by defaultKey and provides a sensible fallback when folderId is not present in search params. The resolution order (search params → default → undefined) is clear and appropriate.

Also applies to: 23-23, 38-39

packages/prisma/schema/folder.prisma (1)

31-31: LGTM! Proper bidirectional relationship.

The optional dashboard relation correctly establishes the Folder side of the one-to-one relationship with Dashboard, complementing the folder relation added to the Dashboard model.

apps/web/lib/swr/use-folder.ts (1)

26-26: LGTM! Useful error exposure.

Exposing the error field from SWR enables consuming components to handle folder fetch errors appropriately. This aligns with the broader folder-aware dashboard flows introduced in this PR.

Also applies to: 40-40

apps/web/app/app.dub.co/(dashboard)/[slug]/links/page-client.tsx (1)

102-120: LGTM! Clean integration of folder dashboard sharing.

The ShareDashboardButton component properly integrates folder-based dashboard sharing with responsive behavior using useMediaQuery.

apps/web/ui/analytics/toggle.tsx (2)

201-221: LGTM! Preserved existing link dashboard display.

The link branch maintains the existing display logic for domain/key-based dashboards.


194-200: No action needed – type safety is enforced by the discriminated union.

The AnalyticsDashboardProps type uses a discriminated union that guarantees folderName is always defined (as a required string) whenever folderId is present. The current code is type-safe and renders correctly without a fallback.

packages/prisma/schema/dashboard.prisma (1)

7-8: LGTM! Proper one-to-one relationship with appropriate constraints.

The folder relation correctly establishes the Dashboard side of the bidirectional relationship. The @unique constraint on folderId and cascade options (onUpdate: Cascade, onDelete: Cascade) ensure proper one-to-one semantics and referential integrity.

apps/web/app/api/folders/[folderId]/dashboard/route.ts (1)

8-32: LGTM! Follows established patterns with proper permission checks.

The endpoint correctly implements folder dashboard retrieval with appropriate access verification. Note that folders.read permission is checked both in the withWorkspace wrapper (line 30) and within verifyFolderAccess (line 16), but this redundancy is harmless and provides defense in depth.

apps/web/lib/swr/use-folder-link-count.ts (1)

5-10: LGTM! Useful conditional fetching enhancement.

The optional enabled parameter (defaulting to true) allows consumers to conditionally disable the link count fetch while preserving backward compatibility. This aligns with the enabled parameter pattern used in useLinksCount.

Also applies to: 13-13

apps/web/lib/zod/schemas/dashboard.ts (1)

17-21: Well-structured union schema for dashboard creation.

The discriminated union pattern using domainKeySchema.or(z.object({ folderId: z.string() })) cleanly supports both link-based and folder-based dashboard creation paths.

apps/web/ui/analytics/share-button.tsx (1)

7-21: LGTM!

The conditional rendering logic correctly handles both link-based and folder-based sharing scenarios with proper permission checks. The early return for partnerPage simplifies the component flow.

apps/web/app/api/dashboards/route.ts (1)

92-97: Verify the intended scope of publicStats update for folder dashboards.

This updateMany sets publicStats = true for all links in the folder. This is a potentially broad update that could affect many links. Ensure this is the intended behavior and consider documenting this side effect for users.

apps/web/ui/analytics/analytics-provider.tsx (2)

36-54: Well-designed discriminated union type for dashboard props.

The AnalyticsDashboardProps type correctly uses never constraints to enforce mutual exclusivity between link-based (domain, key, url) and folder-based (folderId, folderName) variants. This provides strong type safety at the component boundary.


203-219: LGTM!

The useAnalyticsQuery integration properly supports folder-based filtering by accepting defaultFolderId from dashboard props and returning folderId for context propagation.

apps/web/ui/modals/share-dashboard-modal.tsx (4)

33-48: LGTM!

The discriminated union types for ShareDashboardModalInnerProps and ShareDashboardModalProps are well-structured, ensuring type safety when switching between link-based and folder-based dashboard sharing.


78-88: Folder-first priority in dashboard fetching.

The SWR key logic correctly prioritizes folder.id over link.id, ensuring folder dashboards are fetched when a folder context exists.


408-463: Well-implemented FolderCard component.

The component properly handles loading states, error states, and the "mega" folder edge case. The use of useFolderLinkCount with proper enabled conditional prevents unnecessary API calls.


267-276: Good refactor to Controller for form switches.

Using Controller from react-hook-form for the showConversions and doIndex switches is appropriate since these are controlled components that need explicit state management via the fn prop.

apps/web/ui/analytics/use-analytics-filters.tsx (2)

372-391: Domain filter extraction and favicon reuse look good

Centralizing DomainFilterItem and reusing GOOGLE_FAVICON_URL from @dub/utils keeps the behavior identical to the prior inline implementation while removing duplication. The options mapping and getFilterOptionTotal usage remain consistent with the existing domains shape, so this refactor looks safe.


427-430: Double‑check dashboard filter set for link vs. folder dashboards

With the new branch:

  • dashboardProps && dashboardProps.key → no Domain or Link filters.
  • dashboardProps && !dashboardProps.key (likely folder dashboards) → only DomainFilterItem + LinkFilterItem, without the folderId/tagIds/Link type filters that exist in the non‑dashboard folder view.

This may be exactly what you want (folder context implied by the dashboard, so you only expose per‑link/domain refinement), but it does mean:

  • Any active folderId filter coming from the URL has no explicit filter config in this branch.
  • Folder dashboards won’t expose the Folder selector itself, unlike the generic analytics view.

Can you confirm this UX is intentional for both public and internal folder dashboards, and that Filter.Select behaves correctly when activeFilters contains folderId but there is no corresponding visible filter item?

Also applies to: 527-528

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

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

139-139: Optional chaining is defensively safe.

The expression folder?.id || link?.id uses optional chaining, though based on the control flow, one of these values should always be defined (folder errors at line 56-61 if missing dashboard; link errors at line 99-104 if missing dashboard, or is set from demoLink at line 74-77).

This defensive approach is acceptable, though you could replace it with non-null assertions if you prefer to enforce the invariant.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 904f175 and 8f74c9e.

📒 Files selected for processing (1)
  • apps/web/app/api/analytics/dashboard/route.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 3136
File: apps/web/ui/layout/sidebar/app-sidebar-nav.tsx:377-379
Timestamp: 2025-11-19T17:26:51.932Z
Learning: In the dub repository, TWilson023 prefers an incremental approach to route updates: fixing the most user-visible issues first (like sidebar navigation) while deferring less visible internal references to follow-up PRs, especially when redirects provide backward compatibility.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-06-18T20:26:25.177Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2538
File: apps/web/ui/partners/overview/blocks/commissions-block.tsx:16-27
Timestamp: 2025-06-18T20:26:25.177Z
Learning: In the Dub codebase, components that use workspace data (workspaceId, defaultProgramId) are wrapped in `WorkspaceAuth` which ensures these values are always available, making non-null assertions safe. This is acknowledged as a common pattern in their codebase, though not ideal.

Applied to files:

  • apps/web/app/api/analytics/dashboard/route.ts
🧬 Code graph analysis (1)
apps/web/app/api/analytics/dashboard/route.ts (4)
apps/web/lib/api/errors.ts (1)
  • DubApiError (58-75)
packages/utils/src/constants/dub-domains.ts (1)
  • DUB_DEMO_LINKS (157-212)
apps/web/lib/api/utils/assert-valid-date-range-for-plan.ts (1)
  • assertValidDateRangeForPlan (4-21)
apps/web/lib/exceeded-limit-error.ts (1)
  • exceededLimitError (4-29)
⏰ 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: build
🔇 Additional comments (5)
apps/web/app/api/analytics/dashboard/route.ts (5)

21-29: LGTM! Parameter validation correctly handles both contexts.

The validation logic properly requires either domain/key (for link-based analytics) or folderId (for folder-based analytics), with a clear error message for missing parameters.


66-108: Link branch logic is correct.

The code properly handles both demo links (public, no workspace) and regular links (with dashboard validation). Demo links intentionally skip workspace-related checks.

Note: The previous review issues regarding missing id and createdAt in the project select have been correctly fixed (lines 89, 93).


110-116: Date range validation correctly resolved.

The validation now properly uses workspace?.createdAt for dataAvailableFrom. The previous review issue about missing createdAt in project selects has been correctly fixed (lines 49, 93 add createdAt: true).


118-127: LGTM! Usage limit check properly handles demo links.

The conditional check ensures demo links (where workspace is undefined) skip usage validation while enforcing limits for workspace-backed analytics.


149-155: Response construction is correct.

The conditional spreads properly include workspaceId, folderId, and linkId based on context. The previous review issue about missing workspace.id has been correctly fixed (lines 45, 89 add id: true to project selects).

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 (1)
apps/web/ui/folders/folder-actions.tsx (1)

169-184: Button nested inside Link (previously flagged).

This accessibility issue was previously identified and deferred. Nesting a <button> inside an <a> creates invalid HTML and can cause accessibility problems. However, consistent with your incremental approach, this refactoring remains out of scope for the folder analytics feature.

Based on learnings: TWilson023 prefers to keep refactoring separate from feature PRs when the code pattern was existing and is being preserved during restructuring.

🧹 Nitpick comments (2)
apps/web/ui/folders/simple-folder-card.tsx (1)

1-34: Solid, type-safe folder card; only minor optional polish

The component looks correct and nicely reuses FolderIcon with a responsive, truncating layout. One optional improvement would be to pull the props definition into a named type or interface (e.g. type SimpleFolderCardProps = { folder: FolderSummary; className?: string }) and reuse it where needed for consistency. Also, double‑check that importing cn from "@dub/utils/src" matches how other components in this repo import it (some projects standardize on "@dub/utils").

apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx (1)

7-7: Prefer import type { Metadata } for type-only usage

Metadata is only used as a TypeScript return type in generateMetadata, so importing it as a type avoids an unnecessary runtime import and better matches Next’s recommended pattern.

You can simplify to:

-import { Metadata } from "next";
+import type { Metadata } from "next";

Please confirm this matches the current Next 15+ docs and your project’s TS import conventions.

Also applies to: 15-15

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d48862 and ea6e78b.

📒 Files selected for processing (6)
  • apps/web/app/api/og/analytics/route.tsx (4 hunks)
  • apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx (3 hunks)
  • apps/web/lib/fetchers/get-dashboard.ts (1 hunks)
  • apps/web/ui/folders/folder-actions.tsx (6 hunks)
  • apps/web/ui/folders/simple-folder-card.tsx (1 hunks)
  • apps/web/ui/modals/set-default-folder-modal.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 3136
File: apps/web/ui/layout/sidebar/app-sidebar-nav.tsx:377-379
Timestamp: 2025-11-19T17:26:51.932Z
Learning: In the dub repository, TWilson023 prefers an incremental approach to route updates: fixing the most user-visible issues first (like sidebar navigation) while deferring less visible internal references to follow-up PRs, especially when redirects provide backward compatibility.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/ui/modals/set-default-folder-modal.tsx
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx
  • apps/web/ui/folders/folder-actions.tsx
📚 Learning: 2025-06-16T19:21:23.506Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the `useAnalyticsFilterOption` function in `apps/web/ui/analytics/utils.ts`, the pattern `options?.context ?? useContext(AnalyticsContext)` is intentionally designed as a complete replacement strategy, not a merge. When `options.context` is provided, it should contain all required fields (`baseApiPath`, `queryString`, `selectedTab`, `requiresUpgrade`) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.

Applied to files:

  • apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx
📚 Learning: 2025-09-24T15:50:16.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.

Applied to files:

  • apps/web/ui/folders/folder-actions.tsx
📚 Learning: 2025-11-19T17:26:51.932Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3136
File: apps/web/ui/layout/sidebar/app-sidebar-nav.tsx:377-379
Timestamp: 2025-11-19T17:26:51.932Z
Learning: In the dub repository, TWilson023 prefers an incremental approach to route updates: fixing the most user-visible issues first (like sidebar navigation) while deferring less visible internal references to follow-up PRs, especially when redirects provide backward compatibility.

Applied to files:

  • apps/web/ui/folders/folder-actions.tsx
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.

Applied to files:

  • apps/web/ui/folders/folder-actions.tsx
🧬 Code graph analysis (5)
apps/web/ui/folders/simple-folder-card.tsx (2)
apps/web/lib/types.ts (1)
  • FolderSummary (539-542)
apps/web/ui/folders/folder-icon.tsx (1)
  • FolderIcon (66-108)
apps/web/ui/modals/set-default-folder-modal.tsx (1)
apps/web/ui/folders/simple-folder-card.tsx (1)
  • SimpleFolderCard (5-34)
apps/web/app/app.dub.co/(share)/share/[dashboardId]/page.tsx (1)
apps/web/lib/fetchers/get-dashboard.ts (1)
  • getDashboard (4-35)
apps/web/ui/folders/folder-actions.tsx (2)
apps/web/ui/modals/share-dashboard-modal.tsx (1)
  • useShareDashboardModal (465-485)
apps/web/lib/folder/constants.ts (1)
  • unsortedLinks (44-49)
apps/web/app/api/og/analytics/route.tsx (3)
apps/web/lib/types.ts (1)
  • Folder (523-523)
apps/web/lib/analytics/utils/get-start-end-dates.ts (1)
  • getStartEndDates (4-46)
packages/utils/src/constants/misc.ts (1)
  • GOOGLE_FAVICON_URL (26-27)
⏰ 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: build
🔇 Additional comments (12)
apps/web/ui/modals/set-default-folder-modal.tsx (3)

15-15: Good reuse of SimpleFolderCard for consistent folder visuals

Importing and using SimpleFolderCard here keeps the folder preview consistent with other surfaces (icon, name, description) and avoids duplicating layout logic. No issues from a typing or composition standpoint.


56-79: Modal body refactor improves clarity and scannability

The split header/body plus the descriptive copy, help link, and scrollable SimpleFolderCard area make the default-folder action much clearer. The constrained max-h with overflow-y-auto is a nice touch to keep the modal from growing too tall. Just ensure the help-center URL remains stable long term, since it’s now a more prominent part of the UX.


81-102: Footer actions and Cancel behavior look correct

Adding an explicit secondary “Cancel” button and aligning button sizing (h-8 w-fit px-3) make the footer clearer and more consistent. Closing the modal on Cancel without mutating state matches the expected “no change on dismiss” pattern from prior learnings, and the primary button still correctly wires through toast.promise and isPending.

apps/web/ui/folders/folder-actions.tsx (3)

11-11: LGTM: ShareDashboardModal integration follows established patterns.

The modal hook initialization and rendering align with existing modal implementations in this component (RenameFolderModal, DeleteFolderModal, DefaultFolderModal).

Also applies to: 24-24, 60-62, 130-130


76-76: LGTM: Keyboard shortcut properly implemented.

The 's' shortcut is correctly guarded with permission checks and doesn't conflict with existing shortcuts.

Also applies to: 89-92


136-165: LGTM: Popover restructure with proper permission guards.

The reorganized action groups are well-structured:

  • Edit/Delete section correctly restricted to non-unsorted folders with appropriate disabled states
  • Share Analytics properly guarded with canUpdateFolder check
  • Copy Folder ID visual feedback (CircleCheck/Copy toggle) enhances UX
  • All permission checks and conditional rendering logic are correct

Also applies to: 186-198, 201-234

apps/web/lib/fetchers/get-dashboard.ts (1)

16-16: Prisma select additions for link and folder look correct and are safely additive

Including link.id and selecting folder.id/folder.name aligns with the new dashboard consumers and doesn’t change existing behavior, since you’re only expanding the selected shape.

Also applies to: 22-27

apps/web/app/api/og/analytics/route.tsx (5)

2-2: LGTM!

The import of Folder and Link types from @dub/prisma/client provides proper type safety for the data fetching logic.


20-27: LGTM with a minor note.

The validation ensures at least one identifier is provided. Note that this logic allows both linkId and folderId to be present simultaneously. If mutual exclusivity is desired, consider adding a check for that scenario.


111-148: LGTM!

The conditional rendering logic correctly distinguishes between folder and link contexts:

  • Folder context: displays a blue folder icon with the folder name
  • Link context: displays the link's favicon and formatted domain/key

The implementation is clear and handles both cases appropriately.


167-167: LGTM!

The label now correctly reflects the 30-day interval configured on line 75, improving consistency between the UI and the actual data being displayed.


78-89: Verification of Tinybird API endpoint version requires external access.

The review request cannot be fully verified from codebase analysis alone. The code shows the v3_timeseries endpoint is currently in use with proper error handling and response structure expectations (array with clicks property). However, confirming endpoint stability, parameter compatibility, and production readiness requires access to Tinybird API documentation or testing credentials, which are not available in the repository.

The parameters (event, workspaceId, folderId, linkId, start, end, granularity) are correctly included in the query, and the response is defensively handled with .catch(() => []) fallback.

@steven-tey steven-tey merged commit 89aa284 into main Dec 3, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the folder-dashboard branch December 3, 2025 02:27
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