Skip to content
Open
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
fd64edb
multi-select ui filter for link analytics
pepeladeira Feb 1, 2026
36dfe29
initial support for advanced filters in link
pepeladeira Feb 2, 2026
f77562e
test: e2e for advanced filters
pepeladeira Feb 2, 2026
5b37146
review advanced analytic schema description
pepeladeira Feb 2, 2026
6dbf7b1
return with stripe instance as it was
pepeladeira Feb 2, 2026
b4ff07c
feat: ensure multi select filter for other attrs
pepeladeira Feb 6, 2026
0cf46ed
Merge branch 'main' into feat/advanced-filters
pepeladeira Feb 6, 2026
1230beb
feat: apply isMultiple again and add filters in Events page
pepeladeira Feb 7, 2026
a0db5f6
feat: apply isMultiple again and add filters in Events page
pepeladeira Feb 7, 2026
f0ac778
feat(advanced-filters): ensure filter component is working for every …
pepeladeira Feb 7, 2026
a2e03b0
test(advanced-filters): fix and create new tests
pepeladeira Feb 7, 2026
cbd9e11
feat(advanced-filters): prevent filter-list from closing after select…
pepeladeira Feb 7, 2026
24638cc
test(events): get-events tests suite
pepeladeira Feb 7, 2026
c27ffe5
feat(advanced-filter): analytics pipe helpers
pepeladeira Feb 7, 2026
35627a2
Merge pull request #1 from pepeladeira/feat/advanced-filters
pepeladeira Feb 7, 2026
e2ef1a8
fix: remove repetitive code
pepeladeira Feb 7, 2026
fa4f077
fix(advanced-filter): remove null checks before referencing stripe
pepeladeira Feb 8, 2026
80b4274
fix(advanced-fiters): Double folderId filter causes contradictory WHE…
pepeladeira Feb 8, 2026
e1027b0
fix(advanced-filters): onRemoveFilter('link') won't clear the link fi…
pepeladeira Feb 8, 2026
00684ac
fix(advanced-filters): Add typeof guard for regionForPipe to match th…
pepeladeira Feb 8, 2026
82b2473
fix(advanced-filters): newParam could be undefined when parsed is und…
pepeladeira Feb 8, 2026
47e11b6
fix(advanced-filters): Address static analysis warning on forEach cal…
pepeladeira Feb 8, 2026
d938d52
fix(advanced-filters): Edge case: { key, values, operator: undefined …
pepeladeira Feb 8, 2026
4b0e1b7
fix(advanced-filters): add field-based JSON filter handling in sale_e…
pepeladeira Feb 8, 2026
6f23007
fix(advanced-filters): add field-based JSON filter handling in sale_e…
pepeladeira Feb 8, 2026
7926796
fix(advanced-filters): onToggleOperator preserve pagination state.
pepeladeira Feb 8, 2026
8326b1a
fix(advanced-filters): pluralize os
pepeladeira Feb 8, 2026
e989305
fix(advanced-filters): appropriate extraction functions for numeric m…
pepeladeira Feb 8, 2026
f03501e
fix(advanced-filters): add field-based JSON filter handling in sale_e…
pepeladeira Feb 8, 2026
d7f39c4
fix(advanced-filters): ensure keyboar toggleValue checkbox for filter…
pepeladeira Feb 8, 2026
08d73a5
fix(advanced-filters): mismatch prevents root filter from rendering s…
pepeladeira Feb 8, 2026
7d45f87
Merge branch 'main' into main
steven-tey Feb 10, 2026
c6ae269
Merge pull request #3436 from pepeladeira/main
pepeladeira Feb 11, 2026
4d68c78
feat(advanced-filters): support for partnerId, groupId, and tenantId
pepeladeira Feb 11, 2026
8a0b78f
fix: address CodeRabbit review - root filter, missing trigger/metadat…
pepeladeira Feb 12, 2026
c28d463
fix(advanced-filter): resolve build type errors - EventsFilters Parti…
pepeladeira Feb 12, 2026
51247bc
Merge branch 'main' into feat/advanced-filters
steven-tey Feb 12, 2026
a4118e3
Merge branch 'main' into feat/advanced-filters
steven-tey Feb 12, 2026
fa0eef2
Fix failed payout status update
steven-tey Feb 11, 2026
715c3d9
Show the program destination URL in the marketplace
marcusljf Feb 11, 2026
8ccfe4d
Coderabbit fix
marcusljf Feb 11, 2026
b7d8e4f
simplify
steven-tey Feb 12, 2026
c931aa1
Update create-program-application.ts
steven-tey Feb 12, 2026
e0b1c03
Return early if detectBot in track/open
steven-tey Feb 12, 2026
06b1c09
improve docs
steven-tey Feb 12, 2026
2cbeb4e
use clickData null instead
steven-tey Feb 12, 2026
868ecc5
Update route.ts
steven-tey Feb 12, 2026
e8e4cb1
fix in-app application flow
steven-tey Feb 12, 2026
e0913b8
update scripts
steven-tey Feb 12, 2026
412314e
improve import scripts
steven-tey Feb 12, 2026
8af3e7d
increase EMAIL_OTP_EXPIRY_IN to 5 mins
steven-tey Feb 13, 2026
3f82ceb
improve getLinksForWorkspace for isMegaWorkspace
steven-tey Feb 13, 2026
3f25815
improve SelectField for program application form
steven-tey Feb 13, 2026
9157151
randomize sort for featured programs
steven-tey Feb 13, 2026
a2b3d05
website globe
steven-tey Feb 13, 2026
91ac839
chore: update w/ main
pepeladeira Feb 13, 2026
6b7b09b
chore: update w/ main
pepeladeira Feb 13, 2026
37ea6bd
Merge branch 'main' into feat/advanced-filters
steven-tey Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/web/app/(ee)/api/admin/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { withAdmin } from "@/lib/auth";
import { analyticsQuerySchema } from "@/lib/zod/schemas/analytics";
import { parseAnalyticsQuery } from "@/lib/zod/schemas/analytics";
import { NextResponse } from "next/server";

// GET /api/admin/analytics – get analytics for admin
export const GET = withAdmin(async ({ searchParams }) => {
const parsedParams = analyticsQuerySchema.parse(searchParams);
const parsedParams = parseAnalyticsQuery(searchParams);

const response = await getAnalytics(parsedParams);

Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/(ee)/api/admin/events/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getEvents } from "@/lib/analytics/get-events";
import { withAdmin } from "@/lib/auth";
import { eventsQuerySchema } from "@/lib/zod/schemas/analytics";
import { parseEventsQuery } from "@/lib/zod/schemas/analytics";
import { NextResponse } from "next/server";

// GET /api/admin/events – get events for admin
export const GET = withAdmin(async ({ searchParams }) => {
const parsedParams = eventsQuerySchema.parse(searchParams);
const parsedParams = parseEventsQuery(searchParams);

const response = await getEvents(parsedParams);

Expand Down
4 changes: 1 addition & 3 deletions apps/web/app/(ee)/api/cron/export/events/workspace/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const payloadSchema = eventsQuerySchema.extend({
userId: z.string(),
linkId: z.string().optional(),
folderIds: z.array(z.string()).optional(),
folderId: z.string().optional(),
dataAvailableFrom: z.string().optional(),
});

Expand Down Expand Up @@ -78,7 +77,7 @@ export async function POST(req: Request) {
);
}

const { linkId, folderIds, folderId, dataAvailableFrom, ...eventFilters } =
const { linkId, folderIds, dataAvailableFrom, ...eventFilters } =
filters;

// Fetch events in batches and build CSV
Expand All @@ -89,7 +88,6 @@ export async function POST(req: Request) {
...(linkId && { linkId }),
workspaceId,
folderIds,
folderId: folderId || "",
dataAvailableFrom: dataAvailableFrom
? new Date(dataAvailableFrom)
: workspace.createdAt,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(ee)/api/cron/usage/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const updateUsage = async () => {
event: "clicks",
groupBy: "top_links",
interval: "30d",
root: false,
root: { values: ["false"], operator: "IS", sqlOperator: "IN" },
});

const topFive = topLinks.slice(0, 5);
Expand Down
19 changes: 15 additions & 4 deletions apps/web/app/(ee)/api/events/export/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
eventsExportColumnAccessors,
eventsExportColumnNames,
} from "@/lib/analytics/events-export-helpers";
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { getEvents } from "@/lib/analytics/get-events";
import { getFolderIdsToFilter } from "@/lib/analytics/get-folder-ids-to-filter";
Expand Down Expand Up @@ -34,8 +35,20 @@ export const GET = withWorkspace(
})
.parse(searchParams);

const { event, domain, interval, start, end, columns, key, folderId } =
parsedParams;
const {
event,
domain: domainFilter,
interval,
start,
end,
columns,
key,
folderId: folderIdFilter,
} = parsedParams;

// Extract string values for specific link/folder lookup
const domain = getFirstFilterValue(domainFilter);
const folderId = getFirstFilterValue(folderIdFilter);

if (domain) {
await getDomainOrThrow({ workspace, domain });
Expand Down Expand Up @@ -100,7 +113,6 @@ export const GET = withWorkspace(
userId: session.user.id,
...(link && { linkId: link.id }),
folderIds: folderIds ? folderIds : undefined,
folderId: folderId || "",
dataAvailableFrom: workspace.createdAt.toISOString(),
},
});
Expand All @@ -114,7 +126,6 @@ export const GET = withWorkspace(
workspaceId: workspace.id,
limit: MAX_EVENTS_TO_EXPORT,
folderIds,
folderId: folderId || "",
});

const data = response.map((row) =>
Expand Down
15 changes: 10 additions & 5 deletions apps/web/app/(ee)/api/events/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getEvents } from "@/lib/analytics/get-events";
import { getFolderIdsToFilter } from "@/lib/analytics/get-folder-ids-to-filter";
import { getDomainOrThrow } from "@/lib/api/domains/get-domain-or-throw";
Expand All @@ -6,7 +7,7 @@ import { throwIfClicksUsageExceeded } from "@/lib/api/links/usage-checks";
import { assertValidDateRangeForPlan } from "@/lib/api/utils/assert-valid-date-range-for-plan";
import { withWorkspace } from "@/lib/auth";
import { verifyFolderAccess } from "@/lib/folder/permissions";
import { eventsQuerySchema } from "@/lib/zod/schemas/analytics";
import { parseEventsQuery } from "@/lib/zod/schemas/analytics";
import { Link } from "@dub/prisma/client";
import { NextResponse } from "next/server";

Expand All @@ -15,7 +16,7 @@ export const GET = withWorkspace(
async ({ searchParams, workspace, session }) => {
throwIfClicksUsageExceeded(workspace);

const parsedParams = eventsQuerySchema.parse(searchParams);
const parsedParams = parseEventsQuery(searchParams);

let {
event,
Expand All @@ -24,11 +25,15 @@ export const GET = withWorkspace(
end,
linkId,
externalId,
domain,
domain: domainFilter,
key,
folderId,
folderId: folderIdFilter,
} = parsedParams;

// Extract string values for specific link/folder lookup
const domain = getFirstFilterValue(domainFilter);
const folderId = getFirstFilterValue(folderIdFilter);

let link: Link | null = null;

if (domain) {
Expand Down Expand Up @@ -78,7 +83,7 @@ export const GET = withWorkspace(
...(link && { linkId: link.id }),
workspaceId: workspace.id,
folderIds,
folderId: folderId || "",
folderId: folderIdFilter || undefined,
});
console.timeEnd("getEvents");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { VALID_ANALYTICS_ENDPOINTS } from "@/lib/analytics/constants";
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { convertToCSV } from "@/lib/analytics/utils";
import { DubApiError } from "@/lib/api/errors";
Expand Down Expand Up @@ -37,7 +38,10 @@ export const GET = withPartnerProfile(

const parsedParams = partnerProfileAnalyticsQuerySchema.parse(searchParams);

let { linkId, domain, key, ...rest } = parsedParams;
let { linkId, domain: domainFilter, key, ...rest } = parsedParams;

// Extract string value for link lookup
const domain = getFirstFilterValue(domainFilter);

if (linkId) {
if (!links.some((link) => link.id === linkId)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { DubApiError } from "@/lib/api/errors";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
Expand All @@ -23,9 +24,11 @@ export const GET = withPartnerProfile(
},
});

let { linkId, domain, key, ...rest } =
let { linkId, domain: domainFilter, key, ...rest } =
partnerProfileAnalyticsQuerySchema.parse(searchParams);

const domain = getFirstFilterValue(domainFilter);

if (linkId) {
if (!links.some((link) => link.id === linkId)) {
throw new DubApiError({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
eventsExportColumnAccessors,
eventsExportColumnNames,
} from "@/lib/analytics/events-export-helpers";
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { getEvents } from "@/lib/analytics/get-events";
import { convertToCSV } from "@/lib/analytics/utils";
Expand Down Expand Up @@ -59,7 +60,9 @@ export const GET = withPartnerProfile(
.parse(searchParams);

const { event, columns: columnsParam } = parsedParams;
let { linkId, domain, key, ...rest } = parsedParams;
let { linkId, domain: domainFilter, key, ...rest } = parsedParams;

const domain = getFirstFilterValue(domainFilter);

// Default columns based on event type if not provided
const defaultColumns: Record<string, string[]> = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getEvents } from "@/lib/analytics/get-events";
import { DubApiError } from "@/lib/api/errors";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
Expand Down Expand Up @@ -38,9 +39,11 @@ export const GET = withPartnerProfile(
});
}

let { linkId, domain, key, ...rest } =
let { linkId, domain: domainFilter, key, ...rest } =
partnerProfileEventsQuerySchema.parse(searchParams);

const domain = getFirstFilterValue(domainFilter);

if (linkId) {
if (!links.some((link) => link.id === linkId)) {
throw new DubApiError({
Expand Down
11 changes: 8 additions & 3 deletions apps/web/app/api/analytics/dashboard/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors";
import { assertValidDateRangeForPlan } from "@/lib/api/utils/assert-valid-date-range-for-plan";
import { exceededLimitError } from "@/lib/exceeded-limit-error";
import { PlanProps } from "@/lib/types";
import { redis } from "@/lib/upstash";
import { analyticsQuerySchema } from "@/lib/zod/schemas/analytics";
import { parseAnalyticsQuery } from "@/lib/zod/schemas/analytics";
import { prisma } from "@dub/prisma";
import { DUB_DEMO_LINKS, DUB_WORKSPACE_ID, getSearchParams } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
Expand All @@ -16,9 +17,13 @@ export const dynamic = "force-dynamic";
export const GET = async (req: Request) => {
try {
const searchParams = getSearchParams(req.url);
const parsedParams = analyticsQuerySchema.parse(searchParams);
const parsedParams = parseAnalyticsQuery(searchParams);

const { domain, key, folderId, interval, start, end } = parsedParams;
const { domain: domainFilter, key, folderId: folderIdFilter, interval, start, end } = parsedParams;

// Extract string values for specific link/folder lookup
const domain = getFirstFilterValue(domainFilter);
const folderId = getFirstFilterValue(folderIdFilter);

if ((!domain || !key) && !folderId) {
throw new DubApiError({
Expand Down
13 changes: 9 additions & 4 deletions apps/web/app/api/analytics/export/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { VALID_ANALYTICS_ENDPOINTS } from "@/lib/analytics/constants";
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { getFolderIdsToFilter } from "@/lib/analytics/get-folder-ids-to-filter";
import { convertToCSV } from "@/lib/analytics/utils";
Expand All @@ -8,7 +9,7 @@ import { throwIfClicksUsageExceeded } from "@/lib/api/links/usage-checks";
import { assertValidDateRangeForPlan } from "@/lib/api/utils/assert-valid-date-range-for-plan";
import { withWorkspace } from "@/lib/auth";
import { verifyFolderAccess } from "@/lib/folder/permissions";
import { analyticsQuerySchema } from "@/lib/zod/schemas/analytics";
import { parseAnalyticsQuery } from "@/lib/zod/schemas/analytics";
import { Link } from "@dub/prisma/client";
import JSZip from "jszip";

Expand All @@ -17,11 +18,15 @@ export const GET = withWorkspace(
async ({ searchParams, workspace, session }) => {
throwIfClicksUsageExceeded(workspace);

const parsedParams = analyticsQuerySchema.parse(searchParams);
const parsedParams = parseAnalyticsQuery(searchParams);

const { interval, start, end, linkId, externalId, domain, key, folderId } =
const { interval, start, end, linkId, externalId, domain: domainFilter, key, folderId: folderIdFilter } =
parsedParams;

// Extract string values for specific link/folder lookup
const domain = getFirstFilterValue(domainFilter);
const folderId = getFirstFilterValue(folderIdFilter);

let link: Link | null = null;

if (domain) {
Expand Down Expand Up @@ -80,7 +85,7 @@ export const GET = withWorkspace(
...(link && { linkId: link.id }),
groupBy: endpoint,
folderIds,
folderId: folderId || "",
folderId: folderIdFilter || undefined,
});

if (!response || response.length === 0) return;
Expand Down
14 changes: 10 additions & 4 deletions apps/web/app/api/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { VALID_ANALYTICS_ENDPOINTS } from "@/lib/analytics/constants";
import { getFirstFilterValue } from "@/lib/analytics/filter-helpers";
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { getFolderIdsToFilter } from "@/lib/analytics/get-folder-ids-to-filter";
import { getDomainOrThrow } from "@/lib/api/domains/get-domain-or-throw";
Expand All @@ -13,7 +14,7 @@ import { withWorkspace } from "@/lib/auth";
import { verifyFolderAccess } from "@/lib/folder/permissions";
import {
analyticsPathParamsSchema,
analyticsQuerySchema,
parseAnalyticsQuery,
} from "@/lib/zod/schemas/analytics";
import { Link } from "@dub/prisma/client";
import { NextResponse } from "next/server";
Expand All @@ -32,7 +33,7 @@ export const GET = withWorkspace(
oldEvent = undefined;
}

const parsedParams = analyticsQuerySchema.parse(searchParams);
const parsedParams = parseAnalyticsQuery(searchParams);

let {
event,
Expand All @@ -42,12 +43,17 @@ export const GET = withWorkspace(
end,
linkId,
externalId,
domain,
domain: domainFilter,
key,
folderId,
folderId: folderIdFilter,
programId,
} = parsedParams;

// Extract string values for specific link/folder lookup
// When domain+key is provided, it's for getting a specific link (not filtering)
const domain = getFirstFilterValue(domainFilter);
const folderId = getFirstFilterValue(folderIdFilter);

let link: Link | null = null;

event = oldEvent || event;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function WorkspaceLinks() {
activeFilters,
onSelect,
onRemove,
onRemoveFilter,
onRemoveAll,
setSearch,
setSelectedFilter,
Expand Down Expand Up @@ -247,6 +248,7 @@ function WorkspaceLinks() {
activeFilters={activeFilters}
onSelect={onSelect}
onRemove={onRemove}
onRemoveFilter={onRemoveFilter}
onRemoveAll={onRemoveAll}
/>
</PageWidthWrapper>
Expand Down
Loading
Loading