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

Skip to content

Conversation

@marcusljf
Copy link
Collaborator

@marcusljf marcusljf commented Dec 18, 2025

Introduces link filtering by enabled features (e.g., conversion tracking, geo targeting, UTM tags) in both the API and the UI. Adds a utility to build Prisma where clauses for these features, updates Zod schemas to accept the new filter, and enhances the UI to allow users to filter links by feature with counts and icons.

Behaves similarly to the link tag filter.

CleanShot 2025-12-18 at 15 04 58@2x CleanShot 2025-12-18 at 15 05 04@2x CleanShot 2025-12-18 at 15 05 14@2x
CleanShot.2025-12-18.at.14.56.17.mp4

Summary by CodeRabbit

  • New Features
    • Added a new "Link feature" filter in the link management interface. Users can now filter links by their enabled features including conversion tracking, custom preview, geo-targeting, UTM tags, A/B testing, tags, comments, platform-specific targeting, expiration, password protection, link cloaking, and search engine indexing. Filter options display dynamic counts.

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

Introduces support for filtering links by enabled features (e.g., conversion tracking, geo targeting, UTM tags) in both the API and the UI. Adds a utility to build Prisma where clauses for these features, updates Zod schemas to accept the new filter, and enhances the UI to allow users to filter links by feature with counts and icons.
@vercel
Copy link
Contributor

vercel bot commented Dec 18, 2025

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

Project Deployment Review Updated (UTC)
dub Ready Ready Preview Dec 18, 2025 11:10pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

This PR adds support for filtering links by their enabled features (such as conversionTracking, geoTargeting, utmTags, etc.) across the API layer, validation schema, data fetching, and UI, enabling users to filter links based on feature availability.

Changes

Cohort / File(s) Summary
API Filtering Layer
apps/web/lib/api/links/get-links-count.ts, apps/web/lib/api/links/get-links-for-workspace.ts
Extended function signatures and interfaces to accept linkFeatures parameter; integrated buildLinkFeaturesWhere utility into Prisma query construction for filtering.
Link Features Utility
apps/web/lib/api/links/utils/build-link-features-where.ts, apps/web/lib/api/links/utils/index.ts
Introduced new utility function that maps feature strings (conversionTracking, geoTargeting, utmTags, etc.) to Prisma filter objects; exported via barrel index.
Data Fetching Layer
apps/web/lib/swr/use-links.ts
Added linkFeatures to the API query field set to retrieve feature data from the endpoint.
Validation Schema
apps/web/lib/zod/schemas/links.ts
Extended LinksQuerySchema with optional linkFeatures parameter accepting comma-separated or array format; includes OpenAPI spec with enumerated feature values.
UI Filter Component
apps/web/ui/links/use-link-filters.tsx
Added new "Link feature" multi-select filter UI; integrated hook to fetch feature counts; wired state management to handle feature filter selection, removal, and query string updates.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Extra attention areas:
    • apps/web/ui/links/use-link-filters.tsx — Verify correct state management for the new multi-select filter and query string parameter handling (comma-separated format)
    • apps/web/lib/api/links/utils/build-link-features-where.ts — Review the feature-to-Prisma-filter mapping logic, especially the OR composition for each feature and the utmTags nested condition
    • Ensure consistent propagation of linkFeatures parameter across all three API endpoints (getLinksCount, getLinksForWorkspace, and related usage in hooks)

Possibly related PRs

Suggested reviewers

  • steven-tey
  • devkiran

Poem

🐰 Filtering whiskers twitch with glee,
Features now flow wild and free,
Conversion, geo, tags so bright,
Through queries hopping left and right,
A feature feast for links to share! 🎉

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 clearly and concisely describes the main change: adding link feature filters to both the UI and API, which accurately reflects the core functionality introduced across all modified files.
✨ 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 link-filters

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

🧹 Nitpick comments (1)
apps/web/ui/links/use-link-filters.tsx (1)

445-448: Consider storing raw counts instead of re-parsing formatted strings.

The sorting logic parses the formatted string (e.g., "1,234") back to an integer. This is fragile and inefficient. Consider storing the raw count alongside the formatted display value:

🔎 Proposed improvement
     return FEATURE_OPTIONS.map((feature, index) => {
       const count = countValues[index];
       const Icon = feature.icon;
       return {
         value: feature.value,
         label: feature.label,
         icon: <Icon className="h-4 w-4" />,
         right: nFormatter(count, { full: true }),
+        count, // Store raw count for sorting
       };
-    }).sort((a, b) => {
-      const countA = parseInt(a.right?.replace(/,/g, "") || "0");
-      const countB = parseInt(b.right?.replace(/,/g, "") || "0");
-      return countB - countA;
-    });
+    }).sort((a, b) => b.count - a.count);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3669b6 and 06130ce.

📒 Files selected for processing (7)
  • apps/web/lib/api/links/get-links-count.ts (3 hunks)
  • apps/web/lib/api/links/get-links-for-workspace.ts (3 hunks)
  • apps/web/lib/api/links/utils/build-link-features-where.ts (1 hunks)
  • apps/web/lib/api/links/utils/index.ts (1 hunks)
  • apps/web/lib/swr/use-links.ts (1 hunks)
  • apps/web/lib/zod/schemas/links.ts (1 hunks)
  • apps/web/ui/links/use-link-filters.tsx (7 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 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/lib/swr/use-links.ts
  • apps/web/lib/api/links/utils/build-link-features-where.ts
  • apps/web/lib/api/links/utils/index.ts
  • apps/web/lib/zod/schemas/links.ts
  • apps/web/lib/api/links/get-links-for-workspace.ts
  • apps/web/ui/links/use-link-filters.tsx
  • apps/web/lib/api/links/get-links-count.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/api/links/get-links-for-workspace.ts
📚 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/links/use-link-filters.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/ui/links/use-link-filters.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/ui/links/use-link-filters.tsx
🧬 Code graph analysis (4)
apps/web/lib/api/links/utils/build-link-features-where.ts (1)
packages/prisma/client.ts (1)
  • Prisma (30-30)
apps/web/lib/api/links/get-links-for-workspace.ts (1)
apps/web/lib/api/links/utils/build-link-features-where.ts (1)
  • buildLinkFeaturesWhere (3-52)
apps/web/ui/links/use-link-filters.tsx (2)
apps/web/lib/swr/use-links-count.ts (1)
  • useLinksCount (10-53)
packages/ui/src/icons/index.tsx (1)
  • Icon (82-82)
apps/web/lib/api/links/get-links-count.ts (1)
apps/web/lib/api/links/utils/build-link-features-where.ts (1)
  • buildLinkFeaturesWhere (3-52)
🪛 Biome (2.1.2)
apps/web/ui/links/use-link-filters.tsx

[error] 421-421: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ 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 (6)
apps/web/lib/api/links/utils/index.ts (1)

1-1: LGTM!

The new utility is properly re-exported through the barrel file, making it available to consumers like get-links-count.ts and get-links-for-workspace.ts.

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

44-55: LGTM!

The linkFeatures parameter is correctly added to the include list, enabling the new filter to be passed through to the API endpoint alongside existing filters.

apps/web/lib/api/links/get-links-count.ts (1)

5-5: LGTM!

The linkFeatures parameter is properly integrated:

  • Imported from the utils barrel file
  • Destructured from params
  • Spread into the linksWhere clause, which correctly returns undefined when empty (no-op spread)

Also applies to: 26-27, 76-77

apps/web/lib/api/links/get-links-for-workspace.ts (1)

6-6: LGTM!

The linkFeatures integration follows the same pattern as get-links-count.ts:

  • Import updated to include buildLinkFeaturesWhere
  • Parameter added to function signature
  • Spread into the Prisma where clause

The implementation is consistent across both query functions.

Also applies to: 42-43, 147-148

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

120-128: LGTM on the filter definition.

The new linkFeatures filter is correctly structured with multiple: true for multi-select behavior, consistent with the tagIds filter pattern.


160-166: LGTM on query parameter handling.

The onSelect, onRemove, and onRemoveAll handlers are correctly extended to manage the linkFeatures query parameter using the same comma-separated pattern as tagIds.

Also applies to: 188-199, 209-209

Comment on lines +47 to +48
default:
return {};
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

Default case returns empty object, which matches all records in an OR clause.

When an unknown feature string is passed, the default case returns {}. In Prisma, an empty object in a where clause matches all records. Combined with OR, this means OR: [{ validCondition }, {}] effectively bypasses the filter entirely.

Consider filtering out invalid features or returning a condition that matches nothing:

🔎 Proposed fix
-  return {
-    OR: linkFeatures.map((feature) => {
+  const conditions = linkFeatures
+    .map((feature) => {
       switch (feature) {
         // ... existing cases ...
         default:
-          return {};
+          return null;
       }
-    }),
-  };
+    })
+    .filter(Boolean);
+
+  if (conditions.length === 0) {
+    return undefined;
+  }
+
+  return { OR: conditions };

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

🤖 Prompt for AI Agents
In apps/web/lib/api/links/utils/build-link-features-where.ts around lines 47-48,
the default branch currently returns an empty object which in Prisma where/OR
will match all records; change this so unknown feature strings do not bypass the
filter by either (A) filtering out unknown/invalid feature values before
constructing the OR array (so they are not included at all), or (B) change the
default return to a condition that matches nothing (for example return { id: {
equals: null } } assuming id is non-nullable) so the OR entry does not make the
whole clause true; implement one of these fixes and ensure callers handle the
filtered/empty OR correctly.

Comment on lines +151 to +187
linkFeatures: z
.union([z.string(), z.array(z.string())])
.transform((v) => (Array.isArray(v) ? v : v.split(",")))
.optional()
.describe("Filter links by enabled features (comma-separated)")
.openapi({
param: {
style: "form",
explode: false,
},
anyOf: [
{
type: "string",
},
{
type: "array",
items: {
type: "string",
enum: [
"conversionTracking",
"customLinkPreview",
"geoTargeting",
"utmTags",
"abTest",
"tags",
"comments",
"iosTargeting",
"androidTargeting",
"expiration",
"password",
"linkCloaking",
"searchEngineIndexing",
],
},
},
],
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for linkFeatures usage and filtering implementation
echo "=== Finding linkFeatures references in codebase ==="
rg -n "linkFeatures" --type ts -A 3 -B 1 | head -100

echo -e "\n=== Searching for feature filtering implementation ==="
rg -n "conversionTracking|customLinkPreview|geoTargeting" --type ts -A 2 -B 1 | head -80

Repository: dubinc/dub

Length of output: 12661


🏁 Script executed:

#!/bin/bash
# Get full implementation of build-link-features-where.ts
echo "=== Full build-link-features-where.ts implementation ==="
cat apps/web/lib/api/links/utils/build-link-features-where.ts

echo -e "\n=== Check if there's error handling for invalid features ==="
rg -n "linkFeatures" apps/web/lib/api/links --type ts -A 5 -B 2

Repository: dubinc/dub

Length of output: 5349


Add runtime validation for feature names.

The linkFeatures field accepts any string without validation, but the OpenAPI spec documents a specific set of allowed features. Invalid features silently produce empty filter conditions (the default case returns {}), causing users to receive incorrect results. Add enum validation to enforce the documented contract.

Define allowed features as a constant and validate with Zod:

+const LINK_FEATURES = [
+  "conversionTracking",
+  "customLinkPreview",
+  "geoTargeting",
+  "utmTags",
+  "abTest",
+  "tags",
+  "comments",
+  "iosTargeting",
+  "androidTargeting",
+  "expiration",
+  "password",
+  "linkCloaking",
+  "searchEngineIndexing",
+] as const;
+
 linkFeatures: z
   .union([z.string(), z.array(z.string())])
   .transform((v) => (Array.isArray(v) ? v : v.split(",")))
+  .refine(
+    (arr) => arr.every((feature) => LINK_FEATURES.includes(feature as any)),
+    {
+      message: `Invalid feature. Allowed features: ${LINK_FEATURES.join(", ")}`,
+    }
+  )
   .optional()
   .describe("Filter links by enabled features (comma-separated)")
   .openapi({
     param: {
       style: "form",
       explode: false,
     },
     anyOf: [
       {
         type: "string",
+        enum: LINK_FEATURES,
       },
       {
         type: "array",
         items: {
           type: "string",
-          enum: [
-            "conversionTracking",
-            "customLinkPreview",
-            "geoTargeting",
-            "utmTags",
-            "abTest",
-            "tags",
-            "comments",
-            "iosTargeting",
-            "androidTargeting",
-            "expiration",
-            "password",
-            "linkCloaking",
-            "searchEngineIndexing",
-          ],
+          enum: LINK_FEATURES,
         },
       },
     ],
   }),

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

Comment on lines +420 to +428
const counts = FEATURE_OPTIONS.map((feature) =>
useLinksCount<number>({
query: {
linkFeatures: [feature.value],
showArchived,
folderId,
},
}),
);
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

Critical: Hook called inside .map() violates React's Rules of Hooks.

useLinksCount is invoked inside FEATURE_OPTIONS.map(), which violates the Rules of Hooks. Hooks must be called unconditionally at the top level of a component or custom hook—never inside loops, conditions, or nested functions. This can cause unpredictable behavior and state corruption.

Consider restructuring to call the hook once with all features, or use a single aggregated query:

🔎 Proposed fix - use a single query with feature counts

One approach is to add a backend endpoint that returns counts for all features in a single request, or restructure to avoid multiple hook calls:

 function useLinkFeatureFilterOptions({ folderId }: { folderId: string }) {
   const { showArchived } = useContext(LinksDisplayContext);

-  const counts = FEATURE_OPTIONS.map((feature) =>
-    useLinksCount<number>({
-      query: {
-        linkFeatures: [feature.value],
-        showArchived,
-        folderId,
-      },
-    }),
-  );
+  // Option 1: Fetch counts for each feature individually at top level
+  const conversionTrackingCount = useLinksCount<number>({
+    query: { linkFeatures: ["conversionTracking"], showArchived, folderId },
+  });
+  const customLinkPreviewCount = useLinksCount<number>({
+    query: { linkFeatures: ["customLinkPreview"], showArchived, folderId },
+  });
+  // ... repeat for each feature
+
+  const counts = [
+    conversionTrackingCount,
+    customLinkPreviewCount,
+    // ... all other counts
+  ];

   const isLoading = counts.some(({ loading }) => loading);
   const countValues = counts.map(({ data }) => data ?? 0);

Alternatively, consider adding a groupBy: "linkFeature" option to the backend to fetch all feature counts in a single request.

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

🧰 Tools
🪛 Biome (2.1.2)

[error] 421-421: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants