Skip to content

Commit 0111ee2

Browse files
authored
Support multiple filters and reflect them in the chart (#194)
1 parent ab04bf6 commit 0111ee2

File tree

12 files changed

+253
-99
lines changed

12 files changed

+253
-99
lines changed

apps/web/src/actions/analytics/__tests__/events.test.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,62 +58,63 @@ describe('getTasks with filters', () => {
5858
it('should include userId filter in query parameters', async () => {
5959
await getTasks({
6060
orgId: 'test-org',
61-
filterType: 'userId',
62-
filterValue: 'filter-user-id',
61+
filters: [
62+
{ type: 'userId', value: 'filter-user-id', label: 'Test User' },
63+
],
6364
limit: 20,
6465
});
6566

6667
expect(mockQuery).toHaveBeenCalledWith(
6768
expect.objectContaining({
6869
query_params: expect.objectContaining({
69-
filterUserId: 'filter-user-id',
70+
filter0: 'filter-user-id',
7071
}),
7172
}),
7273
);
7374

7475
const queryCall = mockQuery.mock.calls[0]?.[0];
75-
expect(queryCall?.query).toContain('AND e.userId = {filterUserId: String}');
76+
expect(queryCall?.query).toContain('AND e.userId = {filter0: String}');
7677
});
7778

7879
it('should include model filter in query parameters', async () => {
7980
await getTasks({
8081
orgId: 'test-org',
81-
filterType: 'model',
82-
filterValue: 'gpt-4',
82+
filters: [{ type: 'model', value: 'gpt-4', label: 'GPT-4' }],
8383
limit: 20,
8484
});
8585

8686
expect(mockQuery).toHaveBeenCalledWith(
8787
expect.objectContaining({
8888
query_params: expect.objectContaining({
89-
filterModel: 'gpt-4',
89+
filter0: 'gpt-4',
9090
}),
9191
}),
9292
);
9393

9494
const queryCall = mockQuery.mock.calls[0]?.[0];
95-
expect(queryCall?.query).toContain('AND e.modelId = {filterModel: String}');
95+
expect(queryCall?.query).toContain('AND e.modelId = {filter0: String}');
9696
});
9797

9898
it('should include repository filter in query parameters', async () => {
9999
await getTasks({
100100
orgId: 'test-org',
101-
filterType: 'repositoryName',
102-
filterValue: 'test-repo',
101+
filters: [
102+
{ type: 'repositoryName', value: 'test-repo', label: 'test-repo' },
103+
],
103104
limit: 20,
104105
});
105106

106107
expect(mockQuery).toHaveBeenCalledWith(
107108
expect.objectContaining({
108109
query_params: expect.objectContaining({
109-
filterRepository: 'test-repo',
110+
filter0: 'test-repo',
110111
}),
111112
}),
112113
);
113114

114115
const queryCall = mockQuery.mock.calls[0]?.[0];
115116
expect(queryCall?.query).toContain(
116-
'AND e.repositoryName = {filterRepository: String}',
117+
'AND e.repositoryName = {filter0: String}',
117118
);
118119
});
119120

@@ -124,16 +125,15 @@ describe('getTasks with filters', () => {
124125
});
125126

126127
const queryCall = mockQuery.mock.calls[0]?.[0];
127-
expect(queryCall?.query).not.toContain('filterUserId');
128-
expect(queryCall?.query).not.toContain('filterModel');
129-
expect(queryCall?.query).not.toContain('filterRepository');
128+
expect(queryCall?.query).not.toContain('filter0');
129+
expect(queryCall?.query).not.toContain('filter1');
130+
expect(queryCall?.query).not.toContain('filter2');
130131
});
131132

132133
it('should return properly formatted tasks with user data', async () => {
133134
const result = await getTasks({
134135
orgId: 'test-org',
135-
filterType: 'model',
136-
filterValue: 'gpt-4',
136+
filters: [{ type: 'model', value: 'gpt-4', label: 'GPT-4' }],
137137
limit: 20,
138138
});
139139

apps/web/src/actions/analytics/events.ts

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from '@roo-code/types';
99

1010
import type { AnyTimePeriod } from '@/types';
11+
import type { Filter } from '@/types/analytics';
12+
import { buildFilterConditions } from '@/types/analytics';
1113
import { analytics } from '@/lib/server';
1214
import { tokenSumSql } from '@/lib';
1315
import { type User, getUsersById } from '@roo-code-cloud/db/server';
@@ -178,10 +180,12 @@ export const getDeveloperUsage = async ({
178180
orgId,
179181
timePeriod = 90,
180182
userId,
183+
filters = [],
181184
}: {
182185
orgId?: string | null;
183186
timePeriod?: AnyTimePeriod;
184187
userId?: string | null;
188+
filters?: Filter[];
185189
}): Promise<DeveloperUsage[]> => {
186190
await authorizeAnalytics({
187191
requestedOrgId: orgId,
@@ -207,6 +211,9 @@ export const getDeveloperUsage = async ({
207211
queryParams.userId = userId;
208212
}
209213

214+
// Build filter conditions using shared helper
215+
const filterClause = buildFilterConditions(filters, queryParams);
216+
210217
const results = await analytics.query({
211218
query: `
212219
SELECT
@@ -221,6 +228,7 @@ export const getDeveloperUsage = async ({
221228
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
222229
AND type IN ({types: Array(String)})
223230
${userFilter}
231+
${filterClause}
224232
GROUP BY 1
225233
`,
226234
format: 'JSONEachRow',
@@ -245,10 +253,12 @@ export const getRepositoryUsage = async ({
245253
orgId,
246254
timePeriod = 90,
247255
userId,
256+
filters = [],
248257
}: {
249258
orgId?: string | null;
250259
timePeriod?: AnyTimePeriod;
251260
userId?: string | null;
261+
filters?: Filter[];
252262
}): Promise<RepositoryUsage[]> => {
253263
await authorizeAnalytics({
254264
requestedOrgId: orgId,
@@ -274,6 +284,9 @@ export const getRepositoryUsage = async ({
274284
queryParams.userId = userId;
275285
}
276286

287+
// Build filter conditions using shared helper
288+
const filterClause = buildFilterConditions(filters, queryParams);
289+
277290
const results = await analytics.query({
278291
query: `
279292
SELECT
@@ -291,6 +304,7 @@ export const getRepositoryUsage = async ({
291304
AND type IN ({types: Array(String)})
292305
AND repositoryName IS NOT NULL
293306
${userFilter}
307+
${filterClause}
294308
GROUP BY repositoryName
295309
`,
296310
format: 'JSONEachRow',
@@ -318,10 +332,12 @@ export const getModelUsage = async ({
318332
orgId,
319333
timePeriod = 90,
320334
userId,
335+
filters = [],
321336
}: {
322337
orgId?: string | null;
323338
timePeriod?: AnyTimePeriod;
324339
userId?: string | null;
340+
filters?: Filter[];
325341
}): Promise<ModelUsage[]> => {
326342
await authorizeAnalytics({
327343
requestedOrgId: orgId,
@@ -349,6 +365,9 @@ export const getModelUsage = async ({
349365
queryParams.userId = userId;
350366
}
351367

368+
// Build filter conditions using shared helper
369+
const filterClause = buildFilterConditions(filters, queryParams);
370+
352371
const results = await analytics.query({
353372
query: `
354373
SELECT
@@ -364,6 +383,7 @@ export const getModelUsage = async ({
364383
AND type IN ({types: Array(String)})
365384
AND modelId IS NOT NULL
366385
${userFilter}
386+
${filterClause}
367387
GROUP BY 1, 2
368388
`,
369389
format: 'JSONEachRow',
@@ -409,8 +429,7 @@ export const getTasks = async ({
409429
skipAuth = false,
410430
limit = 20,
411431
cursor,
412-
filterType,
413-
filterValue,
432+
filters = [],
414433
}: {
415434
orgId?: string | null;
416435
userId?: string | null;
@@ -419,8 +438,7 @@ export const getTasks = async ({
419438
skipAuth?: boolean;
420439
limit?: number;
421440
cursor?: number;
422-
filterType?: 'userId' | 'model' | 'repositoryName';
423-
filterValue?: string;
441+
filters?: Filter[];
424442
}): Promise<TasksResult> => {
425443
let effectiveUserId = userId;
426444

@@ -480,27 +498,8 @@ export const getTasks = async ({
480498
queryParams.cursor = cursor;
481499
}
482500

483-
// Add filter parameters
484-
if (filterType && filterValue) {
485-
if (filterType === 'userId') {
486-
queryParams.filterUserId = filterValue;
487-
} else if (filterType === 'model') {
488-
queryParams.filterModel = filterValue;
489-
} else if (filterType === 'repositoryName') {
490-
queryParams.filterRepository = filterValue;
491-
}
492-
}
493-
494-
// Build filter conditions
495-
const filterConditions = [];
496-
if (filterType === 'userId' && filterValue) {
497-
filterConditions.push('AND e.userId = {filterUserId: String}');
498-
} else if (filterType === 'model' && filterValue) {
499-
filterConditions.push('AND e.modelId = {filterModel: String}');
500-
} else if (filterType === 'repositoryName' && filterValue) {
501-
filterConditions.push('AND e.repositoryName = {filterRepository: String}');
502-
}
503-
const filterClause = filterConditions.join(' ');
501+
// Build filter conditions using shared helper
502+
const filterClause = buildFilterConditions(filters, queryParams, 'e');
504503

505504
// TODO: Handle same-timestamp edge cases
506505
// Currently using only timestamp as cursor, but this can miss/duplicate tasks
@@ -600,10 +599,12 @@ export const getHourlyUsageByUser = async ({
600599
orgId,
601600
timePeriod = 90,
602601
userId,
602+
filters = [],
603603
}: {
604604
orgId?: string | null;
605605
timePeriod?: AnyTimePeriod;
606606
userId?: string | null;
607+
filters?: Filter[];
607608
}): Promise<HourlyUsageByUser[]> => {
608609
const { effectiveUserId } = await authorizeAnalytics({
609610
requestedOrgId: orgId,
@@ -637,6 +638,9 @@ export const getHourlyUsageByUser = async ({
637638
queryParams.userId = effectiveUserId;
638639
}
639640

641+
// Build filter conditions using shared helper
642+
const filterClause = buildFilterConditions(filters, queryParams);
643+
640644
const results = await analytics.query({
641645
query: `
642646
SELECT
@@ -651,6 +655,7 @@ export const getHourlyUsageByUser = async ({
651655
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
652656
AND type IN ({types: Array(String)})
653657
${userFilter}
658+
${filterClause}
654659
GROUP BY 1, 2
655660
ORDER BY hour_utc DESC, userId
656661
`,

apps/web/src/app/(authenticated)/usage/Developers.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ import type { Filter } from './types';
1717

1818
export const Developers = ({
1919
onFilter,
20+
filters = [],
2021
}: {
2122
onFilter: (filter: Filter) => void;
23+
filters?: Filter[];
2224
}) => {
2325
const { orgId } = useAuth();
2426

2527
const { data = [], isPending } = useQuery({
26-
queryKey: ['getDeveloperUsage', orgId],
27-
queryFn: () => getDeveloperUsage({ orgId }),
28+
queryKey: ['getDeveloperUsage', orgId, filters],
29+
queryFn: () => getDeveloperUsage({ orgId, filters }),
2830
enabled: !!orgId,
2931
});
3032

apps/web/src/app/(authenticated)/usage/Models.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ import type { Filter } from './types';
1212

1313
export const Models = ({
1414
onFilter,
15+
filters = [],
1516
}: {
1617
onFilter: (filter: Filter) => void;
18+
filters?: Filter[];
1719
}) => {
1820
const { orgId } = useAuth();
1921

2022
const { data = [], isPending } = useQuery({
21-
queryKey: ['getModelUsage', orgId],
22-
queryFn: () => getModelUsage({ orgId }),
23+
queryKey: ['getModelUsage', orgId, filters],
24+
queryFn: () => getModelUsage({ orgId, filters }),
2325
enabled: !!orgId,
2426
});
2527

apps/web/src/app/(authenticated)/usage/Repositories.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ import type { Filter } from './types';
1818

1919
export const Repositories = ({
2020
onFilter,
21+
filters = [],
2122
}: {
2223
onFilter: (filter: Filter) => void;
24+
filters?: Filter[];
2325
}) => {
2426
const { orgId } = useAuth();
2527

2628
const { data = [], isPending } = useQuery({
27-
queryKey: ['getRepositoryUsage', orgId],
28-
queryFn: () => getRepositoryUsage({ orgId }),
29+
queryKey: ['getRepositoryUsage', orgId, filters],
30+
queryFn: () => getRepositoryUsage({ orgId, filters }),
2931
enabled: !!orgId,
3032
});
3133

0 commit comments

Comments
 (0)