Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
[![Bun](https://img.shields.io/badge/Bun-1.2-blue.svg)](https://bun.sh/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind-3.4-blue.svg)](https://tailwindcss.com/)

[![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)
[![CI/CD](https://github.com/databuddy/databuddy/actions/workflows/ci.yml/badge.svg)](https://github.com/databuddy/databuddy/actions/workflows/ci.yml)
[![Code Coverage](https://img.shields.io/badge/coverage-85%25-green.svg)](https://github.com/databuddy/databuddy/actions/workflows/coverage.yml)
[![Security Scan](https://img.shields.io/badge/security-A%2B-green.svg)](https://github.com/databuddy/databuddy/actions/workflows/security.yml)
Expand Down
8 changes: 3 additions & 5 deletions apps/api/src/query/simple-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,9 @@ export class SimpleQueryBuilder {
for (let i = 0; i < this.request.filters.length; i++) {
const filter = this.request.filters[i];
if (!filter) continue;
if (this.config.allowedFilters?.includes(filter.field)) {
const { clause, params: filterParams } = this.buildFilter(filter, i);
whereClause.push(clause);
Object.assign(params, filterParams);
}
const { clause, params: filterParams } = this.buildFilter(filter, i);
whereClause.push(clause);
Object.assign(params, filterParams);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

Expand Down
22 changes: 9 additions & 13 deletions packages/db/src/clickhouse/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { LogParams, ErrorLogParams, WarnLogParams, Logger, ResponseJSON } from '@clickhouse/client';
import { ClickHouseLogLevel, createClient } from '@clickhouse/client';
import type { ResponseJSON } from '@clickhouse/client';
import { createClient } from '@clickhouse/client';
import type { NodeClickHouseClientConfigOptions } from '@clickhouse/client/dist/config';


Expand Down Expand Up @@ -94,31 +94,27 @@ export async function chQueryWithMeta<T extends Record<string, any>>(
query: string,
params?: Record<string, unknown>
): Promise<ResponseJSON<T>> {
const start = Date.now();
const res = await clickHouse.query({
query,
query_params: params,
});
const beforeParse = Date.now();
const json = await res.json<T>();
const afterParse = Date.now();
const keys = Object.keys(json.data[0] || {});
const response = {
...json,
data: json.data.map((item) => {
return keys.reduce((acc, key) => {
const meta = json.meta?.find((m) => m.name === key);
return Object.assign(acc, {
[key]:
item[key] && meta?.type.includes('Int')
? Number.parseFloat(item[key] as string)
: item[key],
});
}, {} as T);
acc[key] =
item[key] && meta?.type.includes('Int')
? Number.parseFloat(item[key] as string)
: item[key];
return acc;
}, {} as Record<string, any>);
}),
};

return response;
return response as ResponseJSON<T>;
}

export async function chQuery<T extends Record<string, any>>(
Expand Down
18 changes: 3 additions & 15 deletions packages/rpc/src/routers/funnels.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { z } from 'zod';
import { z } from 'zod/v4';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential build-break: sub-path import zod/v4 is non-standard

zod does not expose a stable sub-path named v4; the canonical entry-point is simply "zod". Unless you are publishing your own fork with that sub-path, this will resolve to module-not-found at runtime and during TypeScript emit.

-import { z } from 'zod/v4';
+import { z } from 'zod';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { z } from 'zod/v4';
-import { z } from 'zod/v4';
+import { z } from 'zod';
🤖 Prompt for AI Agents
In packages/rpc/src/routers/funnels.ts at line 1, the import statement uses a
non-standard sub-path 'zod/v4' which can cause module-not-found errors. Change
the import to use the canonical entry point by importing directly from 'zod'
instead of 'zod/v4' to ensure compatibility and prevent build failures.

import { createTRPCRouter, protectedProcedure } from '../trpc';
import { and, eq, isNull, desc, sql } from 'drizzle-orm';
import { escape as sqlEscape } from 'sqlstring';
import { TRPCError } from '@trpc/server';
import { funnelDefinitions, chQuery } from '@databuddy/db';
import { authorizeWebsiteAccess } from '../utils/auth';
import { logger } from '../utils/discord-webhook';
import { parseReferrer } from '../utils/referrer';

// Validation schemas
const funnelStepSchema = z.object({
type: z.enum(['PAGE_VIEW', 'EVENT', 'CUSTOM']),
target: z.string().min(1),
name: z.string().min(1),
conditions: z.record(z.any()).optional(),
conditions: z.record(z.string(), z.any()).optional(),
});

const funnelFilterSchema = z.object({
Expand Down Expand Up @@ -52,7 +50,6 @@ const funnelAnalyticsSchema = z.object({
endDate: z.string().optional(),
});

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

// Utility functions
const buildFilterConditions = (
filters: Array<{ field: string; operator: string; value: string | string[] }>,
paramPrefix: string,
Expand Down Expand Up @@ -117,7 +113,6 @@ const getDefaultDateRange = () => {
};

export const funnelsRouter = createTRPCRouter({
// Get autocomplete data for funnel creation
getAutocomplete: protectedProcedure
.input(analyticsDateRangeSchema)
.query(async ({ ctx, input }) => {
Expand Down Expand Up @@ -236,7 +231,6 @@ export const funnelsRouter = createTRPCRouter({
}
}),

// Get all funnels for a website
list: protectedProcedure
.input(z.object({ websiteId: z.string() }))
.query(async ({ ctx, input }) => {
Expand Down Expand Up @@ -272,7 +266,6 @@ export const funnelsRouter = createTRPCRouter({
}
}),

// Get a specific funnel
getById: protectedProcedure
.input(z.object({ id: z.string(), websiteId: z.string() }))
.query(async ({ ctx, input }) => {
Expand Down Expand Up @@ -311,7 +304,6 @@ export const funnelsRouter = createTRPCRouter({
}
}),

// Create a new funnel
create: protectedProcedure
.input(createFunnelSchema)
.mutation(async ({ ctx, input }) => {
Expand Down Expand Up @@ -708,7 +700,6 @@ export const funnelsRouter = createTRPCRouter({
}>(sessionReferrerQuery, params);


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

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

// Calculate analytics for each referrer group
const referrerAnalytics = [];
for (const [groupKey, group] of referrerGroups) {
const stepCounts = new Map<number, Set<string>>();
Expand Down Expand Up @@ -771,7 +760,6 @@ export const funnelsRouter = createTRPCRouter({
});
}

// AGGREGATE BY DOMAIN
const aggregated = new Map<string, {
parsed: ReturnType<typeof parseReferrer>,
total_users: number,
Expand All @@ -780,7 +768,7 @@ export const funnelsRouter = createTRPCRouter({
conversion_rate_count: number
}>();
for (const { referrer, referrer_parsed, total_users, completed_users, conversion_rate } of referrerAnalytics) {
const key = referrer; // This is the domain, e.g., "github.com"
const key = referrer;
if (!aggregated.has(key)) {
aggregated.set(key, {
parsed: referrer_parsed,
Expand Down
16 changes: 1 addition & 15 deletions packages/rpc/src/routers/goals.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../trpc';
import { and, eq, isNull, desc, sql, inArray } from 'drizzle-orm';
import { escape as sqlEscape } from 'sqlstring';
import { TRPCError } from '@trpc/server';
import { goals, chQuery } from '@databuddy/db';
import { authorizeWebsiteAccess } from '../utils/auth';
import { logger } from '../utils/discord-webhook';

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

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

export const goalsRouter = createTRPCRouter({
// List all goals for a website
list: protectedProcedure
.input(z.object({ websiteId: z.string() }))
.query(async ({ ctx, input }) => {
Expand All @@ -120,7 +116,6 @@ export const goalsRouter = createTRPCRouter({
.orderBy(desc(goals.createdAt));
return result;
}),
// Get a specific goal
getById: protectedProcedure
.input(z.object({ id: z.string(), websiteId: z.string() }))
.query(async ({ ctx, input }) => {
Expand All @@ -139,7 +134,6 @@ export const goalsRouter = createTRPCRouter({
}
return result[0];
}),
// Create a new goal
create: protectedProcedure
.input(createGoalSchema)
.mutation(async ({ ctx, input }) => {
Expand All @@ -162,7 +156,6 @@ export const goalsRouter = createTRPCRouter({

return newGoal;
}),
// Update a goal
update: protectedProcedure
.input(updateGoalSchema)
.mutation(async ({ ctx, input }) => {
Expand Down Expand Up @@ -192,7 +185,6 @@ export const goalsRouter = createTRPCRouter({
.returning();
return updatedGoal;
}),
// Delete a goal (soft delete)
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
Expand Down Expand Up @@ -220,7 +212,6 @@ export const goalsRouter = createTRPCRouter({
));
return { success: true };
}),
// Get goal analytics (conversion rate)
getAnalytics: protectedProcedure
.input(analyticsDateRangeSchema)
.query(async ({ ctx, input }) => {
Expand Down Expand Up @@ -305,7 +296,6 @@ export const goalsRouter = createTRPCRouter({
first_occurrence: number;
}>(analysisQuery, params);

// Process the results to calculate analytics
const sessionEvents = new Map<string, Array<{ step_number: number, step_name: string, first_occurrence: number }>>();
for (const event of rawResults) {
if (!sessionEvents.has(event.session_id)) {
Expand All @@ -317,7 +307,6 @@ export const goalsRouter = createTRPCRouter({
first_occurrence: event.first_occurrence
});
}
// Calculate analytics
const stepCounts = new Map<number, Set<string>>();
for (const [sessionId, events] of sessionEvents) {
events.sort((a, b) => a.first_occurrence - b.first_occurrence);
Expand All @@ -332,7 +321,6 @@ export const goalsRouter = createTRPCRouter({
}
}
}
// Build analytics results
const goalCompletions = stepCounts.get(1)?.size || 0;
const conversion_rate = totalWebsiteUsers > 0 ? (goalCompletions / totalWebsiteUsers) * 100 : 0;
const dropoffs = 0;
Expand All @@ -356,7 +344,6 @@ export const goalsRouter = createTRPCRouter({
steps_analytics: analyticsResults
};
}),
// Bulk analytics for multiple goals
bulkAnalytics: protectedProcedure
.input(z.object({
websiteId: z.string(),
Expand All @@ -381,9 +368,8 @@ export const goalsRouter = createTRPCRouter({
const params: Record<string, unknown> = {
websiteId: input.websiteId,
startDate,
endDate: endDate + ' 23:59:59',
endDate: `${endDate} 23:59:59`,
};
// Get total unique users for the website in the date range
const totalWebsiteUsersQuery = `
SELECT COUNT(DISTINCT session_id) as total_users
FROM analytics.events
Expand Down
1 change: 0 additions & 1 deletion packages/rpc/src/utils/rate-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export class RateLimiter {
}

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

if (typeof newCurrentWindowCount !== 'number') {
Expand Down
3 changes: 1 addition & 2 deletions packages/shared/src/lists/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './referrers';
export * from './bots';
export * from './referrers';
export * from './timezones';
export * from './languages';
Loading