+ {goal.config.pathPattern}
+
+ ) : (
+
+ {goal.config.eventName}
+
+ )}
+
+ {goal.config.eventPropertyKey} ={" "}
+ {String(goal.config.eventPropertyValue)}
+
+ + Create your first conversion goal to start tracking important user + actions. +
++ {body} +
+ ) +}) +FormMessage.displayName = "FormMessage" + +export { + useFormField, + Form, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + FormField, +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a29198a..57482cc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -27,8 +27,7 @@ services: - "9000:9000" volumes: - clickhouse-data:/var/lib/clickhouse - # Reduce clickhouse logging - # - ./clickhouse_config:/etc/clickhouse-server/config.d + - ./clickhouse_config:/etc/clickhouse-server/config.d environment: - CLICKHOUSE_DB=analytics - CLICKHOUSE_USER=default diff --git a/docker-compose.yml b/docker-compose.yml index b780131..32cf66b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,6 @@ services: - "9000:9000" volumes: - clickhouse-data:/var/lib/clickhouse - # Reduce clickhouse logging - ./clickhouse_config:/etc/clickhouse-server/config.d environment: - CLICKHOUSE_DB=analytics diff --git a/server/src/api/analytics/createGoal.ts b/server/src/api/analytics/createGoal.ts new file mode 100644 index 0000000..65448ce --- /dev/null +++ b/server/src/api/analytics/createGoal.ts @@ -0,0 +1,133 @@ +import { FastifyReply, FastifyRequest } from "fastify"; +import { db } from "../../db/postgres/postgres.js"; +import { goals } from "../../db/postgres/schema.js"; +import { getUserHasAccessToSite } from "../../lib/auth-utils.js"; +import { z } from "zod"; + +// Define validation schema for path pattern +const pathPatternSchema = z.string().min(1, "Path pattern cannot be empty"); + +// Define validation schema for event config +const eventConfigSchema = z + .object({ + eventName: z.string().min(1, "Event name cannot be empty"), + eventPropertyKey: z.string().optional(), + eventPropertyValue: z + .union([z.string(), z.number(), z.boolean()]) + .optional(), + }) + .refine( + (data) => { + // If one property matching field is provided, both must be provided + if (data.eventPropertyKey && data.eventPropertyValue === undefined) { + return false; + } + if (data.eventPropertyValue !== undefined && !data.eventPropertyKey) { + return false; + } + return true; + }, + { + message: + "Both eventPropertyKey and eventPropertyValue must be provided together or omitted together", + } + ); + +// Define validation schema for the goal request +const goalSchema = z + .object({ + siteId: z.number().int().positive("Site ID must be a positive integer"), + name: z.string().optional(), + goalType: z.enum(["path", "event"]), + config: z.object({ + pathPattern: z.string().optional(), + eventName: z.string().optional(), + eventPropertyKey: z.string().optional(), + eventPropertyValue: z + .union([z.string(), z.number(), z.boolean()]) + .optional(), + }), + }) + .refine( + (data) => { + if (data.goalType === "path") { + return !!data.config.pathPattern; + } else if (data.goalType === "event") { + return !!data.config.eventName; + } + return false; + }, + { + message: "Configuration must match goal type", + path: ["config"], + } + ); + +type CreateGoalRequest = z.infer