Skip to content

Commit 4d68c78

Browse files
committed
feat(advanced-filters): support for partnerId, groupId, and tenantId
1 parent c6ae269 commit 4d68c78

File tree

13 files changed

+944
-33
lines changed

13 files changed

+944
-33
lines changed

apps/web/lib/analytics/filter-helpers.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,28 @@ export function prepareFiltersForPipe(params: {
6060
}
6161

6262
/**
63-
* Extract workspace link filters (domain, tagIds, folderId, root) into
63+
* Normalize a filter value that may be a plain string (e.g. from partner-profile
64+
* routes) or an already-parsed ParsedFilter into a consistent ParsedFilter.
65+
*
66+
* Useful when callers pass a raw ID string but extractWorkspaceLinkFilters
67+
* expects a ParsedFilter with sqlOperator.
68+
*/
69+
export function ensureParsedFilter(
70+
value: string | ParsedFilter | undefined,
71+
): ParsedFilter | undefined {
72+
if (!value) return undefined;
73+
if (typeof value === "string") {
74+
return {
75+
operator: "IS" as const,
76+
sqlOperator: "IN" as const,
77+
values: [value],
78+
};
79+
}
80+
return value;
81+
}
82+
83+
/**
84+
* Extract workspace link filters (domain, tagIds, folderId, partnerId, root) into
6485
* separate values and operators for Tinybird.
6586
*
6687
* These filters are applied on the workspace_links node in Tinybird,
@@ -70,6 +91,9 @@ export function extractWorkspaceLinkFilters(params: {
7091
domain?: ParsedFilter;
7192
tagIds?: ParsedFilter;
7293
folderId?: ParsedFilter;
94+
partnerId?: ParsedFilter;
95+
groupId?: ParsedFilter;
96+
tenantId?: ParsedFilter;
7397
root?: ParsedFilter;
7498
}) {
7599
const extractFilter = (filter?: ParsedFilter) => ({
@@ -82,6 +106,9 @@ export function extractWorkspaceLinkFilters(params: {
82106
const domain = extractFilter(params.domain);
83107
const tagIds = extractFilter(params.tagIds);
84108
const folderId = extractFilter(params.folderId);
109+
const partnerId = extractFilter(params.partnerId);
110+
const groupId = extractFilter(params.groupId);
111+
const tenantId = extractFilter(params.tenantId);
85112
const root = extractFilter(params.root);
86113

87114
return {
@@ -91,6 +118,12 @@ export function extractWorkspaceLinkFilters(params: {
91118
tagIdsOperator: tagIds.operator,
92119
folderId: folderId.values,
93120
folderIdOperator: folderId.operator,
121+
partnerId: partnerId.values,
122+
partnerIdOperator: partnerId.operator,
123+
groupId: groupId.values,
124+
groupIdOperator: groupId.operator,
125+
tenantId: tenantId.values,
126+
tenantIdOperator: tenantId.operator,
94127
root: root.values,
95128
rootOperator: root.operator,
96129
};

apps/web/lib/analytics/get-analytics.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "./constants";
1515
import {
1616
buildAdvancedFilters,
17+
ensureParsedFilter,
1718
extractWorkspaceLinkFilters,
1819
prepareFiltersForPipe,
1920
} from "./filter-helpers";
@@ -130,16 +131,28 @@ export const getAnalytics = async (params: AnalyticsFilters) => {
130131

131132
const allFilters = [...metadataFilters, ...advancedFilters];
132133

134+
// Normalize partnerId (may be a plain string from partner-profile routes)
135+
const partnerIdFilter = ensureParsedFilter(params.partnerId);
136+
133137
const {
134138
domain: domainParam,
135139
domainOperator,
136140
tagIds: tagIdsParam,
137141
tagIdsOperator,
138142
folderId: folderIdParam,
139143
folderIdOperator,
144+
partnerId: partnerIdParam,
145+
partnerIdOperator,
146+
groupId: groupIdParam,
147+
groupIdOperator,
148+
tenantId: tenantIdParam,
149+
tenantIdOperator,
140150
root: rootParam,
141151
rootOperator,
142-
} = extractWorkspaceLinkFilters(params);
152+
} = extractWorkspaceLinkFilters({
153+
...params,
154+
partnerId: partnerIdFilter,
155+
});
143156

144157
const tinybirdParams: any = {
145158
workspaceId,
@@ -148,9 +161,12 @@ export const getAnalytics = async (params: AnalyticsFilters) => {
148161
folderIds: params.folderIds,
149162
customerId: params.customerId,
150163
programId: params.programId,
151-
partnerId: params.partnerId,
152-
tenantId: params.tenantId,
153-
groupId: params.groupId,
164+
partnerId: partnerIdParam,
165+
partnerIdOperator,
166+
tenantId: tenantIdParam,
167+
tenantIdOperator,
168+
groupId: groupIdParam,
169+
groupIdOperator,
154170
domain: domainParam,
155171
domainOperator,
156172
tagIds: tagIdsParam,

apps/web/lib/analytics/get-events.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from "../zod/schemas/sales";
2424
import {
2525
buildAdvancedFilters,
26+
ensureParsedFilter,
2627
extractWorkspaceLinkFilters,
2728
prepareFiltersForPipe,
2829
} from "./filter-helpers";
@@ -106,16 +107,27 @@ export const getEvents = async (params: EventsFilters) => {
106107

107108
const allFilters = [...metadataFilters, ...advancedFilters];
108109

110+
const partnerIdFilter = ensureParsedFilter(params.partnerId);
111+
109112
const {
110113
domain: domainParam,
111114
domainOperator,
112115
tagIds: tagIdsParam,
113116
tagIdsOperator,
114117
folderId: folderIdParam,
115118
folderIdOperator,
119+
partnerId: partnerIdParam,
120+
partnerIdOperator,
121+
groupId: groupIdParam,
122+
groupIdOperator,
123+
tenantId: tenantIdParam,
124+
tenantIdOperator,
116125
root: rootParam,
117126
rootOperator,
118-
} = extractWorkspaceLinkFilters(params);
127+
} = extractWorkspaceLinkFilters({
128+
...params,
129+
partnerId: partnerIdFilter,
130+
});
119131

120132
const tinybirdParams: any = {
121133
eventType,
@@ -125,9 +137,12 @@ export const getEvents = async (params: EventsFilters) => {
125137
folderIds: params.folderIds,
126138
customerId: params.customerId,
127139
programId: params.programId,
128-
partnerId: params.partnerId,
129-
tenantId: params.tenantId,
130-
groupId: params.groupId,
140+
partnerId: partnerIdParam,
141+
partnerIdOperator,
142+
tenantId: tenantIdParam,
143+
tenantIdOperator,
144+
groupId: groupIdParam,
145+
groupIdOperator,
131146
...(typeof triggerForPipe !== 'object' && triggerForPipe ? { trigger: triggerForPipe } : {}),
132147
...(typeof countryForPipe !== 'object' && countryForPipe ? { country: countryForPipe } : {}),
133148
...(typeof regionForPipe === 'string' ? { region: regionForPipe } : {}),

apps/web/lib/analytics/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ParsedFilter } from "@dub/utils";
12
import * as z from "zod/v4";
23
import {
34
analyticsQuerySchema,
@@ -45,13 +46,15 @@ export type AnalyticsFilters = Partial<Omit<z.infer<typeof analyticsQuerySchema>
4546
folderIds?: string[]; // TODO: remove this once it's been added to the public API
4647
start?: Date | null;
4748
end?: Date | null;
49+
partnerId?: string | ParsedFilter;
4850
};
4951

5052
export type EventsFilters = z.infer<typeof eventsQuerySchema> & {
5153
workspaceId?: string;
5254
dataAvailableFrom?: Date;
5355
customerId?: string;
5456
folderIds?: string[];
57+
partnerId?: string | ParsedFilter;
5558
};
5659

5760
const partnerAnalyticsSchema = analyticsQuerySchema

apps/web/lib/zod/schemas/analytics.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,27 @@ export const analyticsQuerySchema = z.object({
9696
tenantId: z
9797
.string()
9898
.optional()
99+
.transform(parseFilterValue)
99100
.describe(
100-
"The ID of the tenant that created the link inside your system.",
101-
),
101+
"The ID of the tenant that created the link inside your system. " +
102+
"Supports advanced filtering: single value, multiple values (comma-separated), or exclusion (prefix with `-`). " +
103+
"Examples: `tenant_123`, `tenant_123,tenant_456`, `-tenant_789`."
104+
)
105+
.meta({ example: "tenant_123" }),
102106
programId: z
103107
.string()
104108
.optional()
105109
.describe("The ID of the program to retrieve analytics for."),
106110
partnerId: z
107111
.string()
108112
.optional()
109-
.describe("The ID of the partner to retrieve analytics for."),
113+
.transform(parseFilterValue)
114+
.describe(
115+
"The ID of the partner to retrieve analytics for. " +
116+
"Supports advanced filtering: single value, multiple values (comma-separated), or exclusion (prefix with `-`). " +
117+
"Examples: `pn_123`, `pn_123,pn_456`, `-pn_789`."
118+
)
119+
.meta({ example: "pn_123" }),
110120
customerId: z
111121
.string()
112122
.optional()
@@ -298,7 +308,13 @@ export const analyticsQuerySchema = z.object({
298308
groupId: z
299309
.string()
300310
.optional()
301-
.describe("The group ID to retrieve analytics for."),
311+
.transform(parseFilterValue)
312+
.describe(
313+
"The group ID to retrieve analytics for. " +
314+
"Supports advanced filtering: single value, multiple values (comma-separated), or exclusion (prefix with `-`). " +
315+
"Examples: `grp_123`, `grp_123,grp_456`, `-grp_789`."
316+
)
317+
.meta({ example: "grp_123" }),
302318
root: z
303319
.string()
304320
.optional()
@@ -480,11 +496,26 @@ export const analyticsFilterTB = z.object({
480496
.optional()
481497
.describe("Filter for root domain links."),
482498
rootOperator: z.enum(["IN", "NOT IN"]).optional(),
483-
// Program/Partner/Group filters (not using advanced filtering)
499+
// Program/Partner/Group filters (not using advanced filtering for programId/tenantId/groupId)
484500
programId: z.string().optional(),
485-
partnerId: z.string().optional(),
486-
tenantId: z.string().optional(),
487-
groupId: z.string().optional(),
501+
partnerId: z
502+
.union([z.string(), z.array(z.string())])
503+
.transform((v) => (Array.isArray(v) ? v : v.split(",")))
504+
.optional()
505+
.describe("The partner ID(s) to retrieve analytics for (with operator support)."),
506+
partnerIdOperator: z.enum(["IN", "NOT IN"]).optional(),
507+
tenantId: z
508+
.union([z.string(), z.array(z.string())])
509+
.transform((v) => (Array.isArray(v) ? v : v.split(",")))
510+
.optional()
511+
.describe("The tenant ID(s) to retrieve analytics for (with operator support)."),
512+
tenantIdOperator: z.enum(["IN", "NOT IN"]).optional(),
513+
groupId: z
514+
.union([z.string(), z.array(z.string())])
515+
.transform((v) => (Array.isArray(v) ? v : v.split(",")))
516+
.optional()
517+
.describe("The group ID(s) to retrieve analytics for (with operator support)."),
518+
groupIdOperator: z.enum(["IN", "NOT IN"]).optional(),
488519
// Region is a special case - it's the subdivision part of a region code
489520
region: z.string().optional(),
490521
// All dimensional filters now go through the JSON filters parameter

0 commit comments

Comments
 (0)