Skip to content

Commit 8366dad

Browse files
authored
Merge pull request #12 from databuddy-analytics/staging
just wanna see what code rabbit does:)
2 parents ab1d87b + 5a3aa22 commit 8366dad

File tree

8 files changed

+21
-559
lines changed

8 files changed

+21
-559
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
[![Bun](https://img.shields.io/badge/Bun-1.2-blue.svg)](https://bun.sh/)
1111
[![Tailwind CSS](https://img.shields.io/badge/Tailwind-3.4-blue.svg)](https://tailwindcss.com/)
1212

13+
[![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/databuddy-analytics/Databuddy?utm_source=oss&utm_medium=github&utm_campaign=databuddy-analytics%2FDatabuddy&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)](https://coderabbit.ai)
1314
[![CI/CD](https://github.com/databuddy/databuddy/actions/workflows/ci.yml/badge.svg)](https://github.com/databuddy/databuddy/actions/workflows/ci.yml)
1415
[![Code Coverage](https://img.shields.io/badge/coverage-85%25-green.svg)](https://github.com/databuddy/databuddy/actions/workflows/coverage.yml)
1516
[![Security Scan](https://img.shields.io/badge/security-A%2B-green.svg)](https://github.com/databuddy/databuddy/actions/workflows/security.yml)

apps/api/src/query/simple-builder.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export class SimpleQueryBuilder {
1111
) { }
1212

1313
private buildFilter(filter: Filter, index: number): { clause: string, params: Record<string, unknown> } {
14+
if (this.config.allowedFilters && !this.config.allowedFilters.includes(filter.field)) {
15+
throw new Error(`Filter on field '${filter.field}' is not permitted.`);
16+
}
1417
const key = `f${index}`;
1518
const operator = FilterOperators[filter.op];
1619

@@ -117,11 +120,9 @@ export class SimpleQueryBuilder {
117120
for (let i = 0; i < this.request.filters.length; i++) {
118121
const filter = this.request.filters[i];
119122
if (!filter) continue;
120-
if (this.config.allowedFilters?.includes(filter.field)) {
121-
const { clause, params: filterParams } = this.buildFilter(filter, i);
122-
whereClause.push(clause);
123-
Object.assign(params, filterParams);
124-
}
123+
const { clause, params: filterParams } = this.buildFilter(filter, i);
124+
whereClause.push(clause);
125+
Object.assign(params, filterParams);
125126
}
126127
}
127128

packages/db/src/clickhouse/client.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { LogParams, ErrorLogParams, WarnLogParams, Logger, ResponseJSON } from '@clickhouse/client';
2-
import { ClickHouseLogLevel, createClient } from '@clickhouse/client';
1+
import type { ResponseJSON } from '@clickhouse/client';
2+
import { createClient } from '@clickhouse/client';
33
import type { NodeClickHouseClientConfigOptions } from '@clickhouse/client/dist/config';
44

55

@@ -94,31 +94,27 @@ export async function chQueryWithMeta<T extends Record<string, any>>(
9494
query: string,
9595
params?: Record<string, unknown>
9696
): Promise<ResponseJSON<T>> {
97-
const start = Date.now();
9897
const res = await clickHouse.query({
9998
query,
10099
query_params: params,
101100
});
102-
const beforeParse = Date.now();
103101
const json = await res.json<T>();
104-
const afterParse = Date.now();
105102
const keys = Object.keys(json.data[0] || {});
106103
const response = {
107104
...json,
108105
data: json.data.map((item) => {
109106
return keys.reduce((acc, key) => {
110107
const meta = json.meta?.find((m) => m.name === key);
111-
return Object.assign(acc, {
112-
[key]:
113-
item[key] && meta?.type.includes('Int')
114-
? Number.parseFloat(item[key] as string)
115-
: item[key],
116-
});
117-
}, {} as T);
108+
acc[key] =
109+
item[key] && meta?.type.includes('Int')
110+
? Number.parseFloat(item[key] as string)
111+
: item[key];
112+
return acc;
113+
}, {} as Record<string, any>);
118114
}),
119115
};
120116

121-
return response;
117+
return response as ResponseJSON<T>;
122118
}
123119

124120
export async function chQuery<T extends Record<string, any>>(

packages/rpc/src/routers/funnels.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import { z } from 'zod';
1+
import { z } from 'zod/v4';
22
import { createTRPCRouter, protectedProcedure } from '../trpc';
33
import { and, eq, isNull, desc, sql } from 'drizzle-orm';
4-
import { escape as sqlEscape } from 'sqlstring';
54
import { TRPCError } from '@trpc/server';
65
import { funnelDefinitions, chQuery } from '@databuddy/db';
76
import { authorizeWebsiteAccess } from '../utils/auth';
87
import { logger } from '../utils/discord-webhook';
98
import { parseReferrer } from '../utils/referrer';
109

11-
// Validation schemas
1210
const funnelStepSchema = z.object({
1311
type: z.enum(['PAGE_VIEW', 'EVENT', 'CUSTOM']),
1412
target: z.string().min(1),
1513
name: z.string().min(1),
16-
conditions: z.record(z.any()).optional(),
14+
conditions: z.record(z.string(), z.any()).optional(),
1715
});
1816

1917
const funnelFilterSchema = z.object({
@@ -52,7 +50,6 @@ const funnelAnalyticsSchema = z.object({
5250
endDate: z.string().optional(),
5351
});
5452

55-
// Security - allowed fields for filtering
5653
const ALLOWED_FIELDS = new Set([
5754
'id', 'client_id', 'event_name', 'anonymous_id', 'time', 'session_id',
5855
'event_type', 'event_id', 'session_start_time', 'timestamp',
@@ -74,7 +71,6 @@ const ALLOWED_OPERATORS = new Set([
7471
'equals', 'contains', 'not_equals', 'in', 'not_in',
7572
]);
7673

77-
// Utility functions
7874
const buildFilterConditions = (
7975
filters: Array<{ field: string; operator: string; value: string | string[] }>,
8076
paramPrefix: string,
@@ -117,7 +113,6 @@ const getDefaultDateRange = () => {
117113
};
118114

119115
export const funnelsRouter = createTRPCRouter({
120-
// Get autocomplete data for funnel creation
121116
getAutocomplete: protectedProcedure
122117
.input(analyticsDateRangeSchema)
123118
.query(async ({ ctx, input }) => {
@@ -236,7 +231,6 @@ export const funnelsRouter = createTRPCRouter({
236231
}
237232
}),
238233

239-
// Get all funnels for a website
240234
list: protectedProcedure
241235
.input(z.object({ websiteId: z.string() }))
242236
.query(async ({ ctx, input }) => {
@@ -272,7 +266,6 @@ export const funnelsRouter = createTRPCRouter({
272266
}
273267
}),
274268

275-
// Get a specific funnel
276269
getById: protectedProcedure
277270
.input(z.object({ id: z.string(), websiteId: z.string() }))
278271
.query(async ({ ctx, input }) => {
@@ -311,7 +304,6 @@ export const funnelsRouter = createTRPCRouter({
311304
}
312305
}),
313306

314-
// Create a new funnel
315307
create: protectedProcedure
316308
.input(createFunnelSchema)
317309
.mutation(async ({ ctx, input }) => {
@@ -708,7 +700,6 @@ export const funnelsRouter = createTRPCRouter({
708700
}>(sessionReferrerQuery, params);
709701

710702

711-
// Group events by session
712703
const sessionEvents = new Map<string, Array<{
713704
step_number: number,
714705
step_name: string,
@@ -723,7 +714,6 @@ export const funnelsRouter = createTRPCRouter({
723714
sessionEvents.get(event.session_id)?.push(event);
724715
}
725716

726-
// Group sessions strictly by lowercased domain (fallback to 'direct')
727717
const referrerGroups = new Map<string, { parsed: ReturnType<typeof parseReferrer>, sessionIds: Set<string> }>();
728718
for (const [sessionId, events] of sessionEvents) {
729719
if (events.length > 0) {
@@ -740,7 +730,6 @@ export const funnelsRouter = createTRPCRouter({
740730
}
741731
}
742732

743-
// Calculate analytics for each referrer group
744733
const referrerAnalytics = [];
745734
for (const [groupKey, group] of referrerGroups) {
746735
const stepCounts = new Map<number, Set<string>>();
@@ -771,7 +760,6 @@ export const funnelsRouter = createTRPCRouter({
771760
});
772761
}
773762

774-
// AGGREGATE BY DOMAIN
775763
const aggregated = new Map<string, {
776764
parsed: ReturnType<typeof parseReferrer>,
777765
total_users: number,
@@ -780,7 +768,7 @@ export const funnelsRouter = createTRPCRouter({
780768
conversion_rate_count: number
781769
}>();
782770
for (const { referrer, referrer_parsed, total_users, completed_users, conversion_rate } of referrerAnalytics) {
783-
const key = referrer; // This is the domain, e.g., "github.com"
771+
const key = referrer;
784772
if (!aggregated.has(key)) {
785773
aggregated.set(key, {
786774
parsed: referrer_parsed,

packages/rpc/src/routers/goals.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { z } from 'zod';
22
import { createTRPCRouter, protectedProcedure } from '../trpc';
33
import { and, eq, isNull, desc, sql, inArray } from 'drizzle-orm';
4-
import { escape as sqlEscape } from 'sqlstring';
54
import { TRPCError } from '@trpc/server';
65
import { goals, chQuery } from '@databuddy/db';
76
import { authorizeWebsiteAccess } from '../utils/auth';
8-
import { logger } from '../utils/discord-webhook';
97

108
const goalSchema = z.object({
119
type: z.enum(['PAGE_VIEW', 'EVENT', 'CUSTOM']),
@@ -66,7 +64,6 @@ const ALLOWED_OPERATORS = new Set([
6664
'equals', 'contains', 'not_equals', 'in', 'not_in',
6765
]);
6866

69-
// Refactored buildFilterConditions to use parameterized values
7067
const buildFilterConditions = (filters: Array<{ field: string; operator: string; value: string | string[] }>, paramPrefix: string, params: Record<string, unknown>) => {
7168
if (!filters || filters.length === 0) return '';
7269
const filterConditions = filters.map((filter, i) => {
@@ -105,7 +102,6 @@ const getDefaultDateRange = () => {
105102
};
106103

107104
export const goalsRouter = createTRPCRouter({
108-
// List all goals for a website
109105
list: protectedProcedure
110106
.input(z.object({ websiteId: z.string() }))
111107
.query(async ({ ctx, input }) => {
@@ -120,7 +116,6 @@ export const goalsRouter = createTRPCRouter({
120116
.orderBy(desc(goals.createdAt));
121117
return result;
122118
}),
123-
// Get a specific goal
124119
getById: protectedProcedure
125120
.input(z.object({ id: z.string(), websiteId: z.string() }))
126121
.query(async ({ ctx, input }) => {
@@ -139,7 +134,6 @@ export const goalsRouter = createTRPCRouter({
139134
}
140135
return result[0];
141136
}),
142-
// Create a new goal
143137
create: protectedProcedure
144138
.input(createGoalSchema)
145139
.mutation(async ({ ctx, input }) => {
@@ -162,7 +156,6 @@ export const goalsRouter = createTRPCRouter({
162156

163157
return newGoal;
164158
}),
165-
// Update a goal
166159
update: protectedProcedure
167160
.input(updateGoalSchema)
168161
.mutation(async ({ ctx, input }) => {
@@ -192,7 +185,6 @@ export const goalsRouter = createTRPCRouter({
192185
.returning();
193186
return updatedGoal;
194187
}),
195-
// Delete a goal (soft delete)
196188
delete: protectedProcedure
197189
.input(z.object({ id: z.string() }))
198190
.mutation(async ({ ctx, input }) => {
@@ -220,7 +212,6 @@ export const goalsRouter = createTRPCRouter({
220212
));
221213
return { success: true };
222214
}),
223-
// Get goal analytics (conversion rate)
224215
getAnalytics: protectedProcedure
225216
.input(analyticsDateRangeSchema)
226217
.query(async ({ ctx, input }) => {
@@ -305,7 +296,6 @@ export const goalsRouter = createTRPCRouter({
305296
first_occurrence: number;
306297
}>(analysisQuery, params);
307298

308-
// Process the results to calculate analytics
309299
const sessionEvents = new Map<string, Array<{ step_number: number, step_name: string, first_occurrence: number }>>();
310300
for (const event of rawResults) {
311301
if (!sessionEvents.has(event.session_id)) {
@@ -317,7 +307,6 @@ export const goalsRouter = createTRPCRouter({
317307
first_occurrence: event.first_occurrence
318308
});
319309
}
320-
// Calculate analytics
321310
const stepCounts = new Map<number, Set<string>>();
322311
for (const [sessionId, events] of sessionEvents) {
323312
events.sort((a, b) => a.first_occurrence - b.first_occurrence);
@@ -332,7 +321,6 @@ export const goalsRouter = createTRPCRouter({
332321
}
333322
}
334323
}
335-
// Build analytics results
336324
const goalCompletions = stepCounts.get(1)?.size || 0;
337325
const conversion_rate = totalWebsiteUsers > 0 ? (goalCompletions / totalWebsiteUsers) * 100 : 0;
338326
const dropoffs = 0;
@@ -356,7 +344,6 @@ export const goalsRouter = createTRPCRouter({
356344
steps_analytics: analyticsResults
357345
};
358346
}),
359-
// Bulk analytics for multiple goals
360347
bulkAnalytics: protectedProcedure
361348
.input(z.object({
362349
websiteId: z.string(),
@@ -381,9 +368,8 @@ export const goalsRouter = createTRPCRouter({
381368
const params: Record<string, unknown> = {
382369
websiteId: input.websiteId,
383370
startDate,
384-
endDate: endDate + ' 23:59:59',
371+
endDate: `${endDate} 23:59:59`,
385372
};
386-
// Get total unique users for the website in the date range
387373
const totalWebsiteUsersQuery = `
388374
SELECT COUNT(DISTINCT session_id) as total_users
389375
FROM analytics.events

packages/rpc/src/utils/rate-limit.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ export class RateLimiter {
4646
}
4747

4848
const prevWindowCount = results[0]?.[1] ? Number.parseInt(results[0][1] as string) : 0
49-
const currentWindowCount = results[1]?.[1] ? Number.parseInt(results[1][1] as string) : 0
5049
const newCurrentWindowCount = results[2]?.[1] as number
5150

5251
if (typeof newCurrentWindowCount !== 'number') {

packages/shared/src/lists/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export * from './referrers';
21
export * from './bots';
2+
export * from './referrers';
33
export * from './timezones';
4-
export * from './languages';

0 commit comments

Comments
 (0)