-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add link feature filters to links UI and API #3247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
📒 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.tsapps/web/lib/api/links/utils/build-link-features-where.tsapps/web/lib/api/links/utils/index.tsapps/web/lib/zod/schemas/links.tsapps/web/lib/api/links/get-links-for-workspace.tsapps/web/ui/links/use-link-filters.tsxapps/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.tsandget-links-for-workspace.ts.apps/web/lib/swr/use-links.ts (1)
44-55: LGTM!The
linkFeaturesparameter 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
linkFeaturesparameter is properly integrated:
- Imported from the utils barrel file
- Destructured from params
- Spread into the
linksWhereclause, which correctly returnsundefinedwhen 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
linkFeaturesintegration follows the same pattern asget-links-count.ts:
- Import updated to include
buildLinkFeaturesWhere- Parameter added to function signature
- Spread into the Prisma
whereclauseThe 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
linkFeaturesfilter is correctly structured withmultiple: truefor multi-select behavior, consistent with thetagIdsfilter pattern.
160-166: LGTM on query parameter handling.The
onSelect,onRemove, andonRemoveAllhandlers are correctly extended to manage thelinkFeaturesquery parameter using the same comma-separated pattern astagIds.Also applies to: 188-199, 209-209
| default: | ||
| return {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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", | ||
| ], | ||
| }, | ||
| }, | ||
| ], | ||
| }), |
There was a problem hiding this comment.
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 -80Repository: 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 2Repository: 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.
| const counts = FEATURE_OPTIONS.map((feature) => | ||
| useLinksCount<number>({ | ||
| query: { | ||
| linkFeatures: [feature.value], | ||
| showArchived, | ||
| folderId, | ||
| }, | ||
| }), | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
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.14.56.17.mp4
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.