From cbf47d58dc991fdd5ab8ea3616170d9a1cde8f03 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 5 Feb 2026 09:13:18 -0800 Subject: [PATCH 1/4] use website_event_stats_hourly view for excludeBounceQuery --- src/lib/clickhouse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 3160dbfea..77357f8ba 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -138,12 +138,12 @@ function getExcludeBounceQuery(filters: Record) { return `join (select distinct session_id, visit_id - from website_event + from website_event_stats_hourly where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} and event_type = 1 group by session_id, visit_id - having count(*) > 1 + having sum(views) > 1 ) excludeBounce on excludeBounce.session_id = website_event.session_id and excludeBounce.visit_id = website_event.visit_id From 306aafd9525b41fb059d3a725c042e1a05b237a8 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 5 Feb 2026 09:35:29 -0800 Subject: [PATCH 2/4] Fix share url. --- .../[websiteId]/settings/ShareEditForm.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx index 4b86247ac..00598fcfd 100644 --- a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx @@ -40,10 +40,7 @@ export function ShareEditForm({ const isEditing = !!shareId; const getUrl = (slug: string) => { - if (cloudMode) { - return `${process.env.cloudUrl}/share/${slug}`; - } - return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`; + return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`; }; useEffect(() => { @@ -74,9 +71,16 @@ export function ShareEditForm({ try { if (isEditing) { - await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, parameters }); + await post(`/share/id/${shareId}`, { + name: data.name, + slug: share.slug, + parameters, + }); } else { - await post(`/websites/${websiteId}/shares`, { name: data.name, parameters }); + await post(`/websites/${websiteId}/shares`, { + name: data.name, + parameters, + }); } touch('shares'); onSave?.(); From fb6fd293fb1b87486c65ec22975918ef20b0ddf0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 8 Feb 2026 22:58:25 -0800 Subject: [PATCH 3/4] Updated zod validation on date range. --- package.json | 2 +- .../[websiteId]/events/stats/route.ts | 6 ++---- .../api/websites/[websiteId]/export/route.ts | 6 ++---- .../[websiteId]/metrics/expanded/route.ts | 5 ++--- .../api/websites/[websiteId]/metrics/route.ts | 5 ++--- .../websites/[websiteId]/pageviews/route.ts | 6 ++---- .../websites/[websiteId]/sessions/route.ts | 6 ++---- .../api/websites/[websiteId]/stats/route.ts | 6 ++---- .../api/websites/[websiteId]/values/route.ts | 6 ++---- src/lib/schema.ts | 19 +++++++++++++++++++ 10 files changed, 36 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index d162be63a..6e0645b5f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "type": "module", "scripts": { - "dev": "next dev -p 3002 --turbo", + "dev": "next dev -p 3009 --turbo", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", diff --git a/src/app/api/websites/[websiteId]/events/stats/route.ts b/src/app/api/websites/[websiteId]/events/stats/route.ts index 61e151d4a..6d0a04602 100644 --- a/src/app/api/websites/[websiteId]/events/stats/route.ts +++ b/src/app/api/websites/[websiteId]/events/stats/route.ts @@ -1,7 +1,6 @@ -import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { dateRangeParams, filterParams } from '@/lib/schema'; +import { filterParams, withDateRange } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getWebsiteEventStats } from '@/queries/sql/events/getWebsiteEventStats'; @@ -9,8 +8,7 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const schema = z.object({ - ...dateRangeParams, + const schema = withDateRange({ ...filterParams, }); diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts index eec81c6d4..1beb9bcb2 100644 --- a/src/app/api/websites/[websiteId]/export/route.ts +++ b/src/app/api/websites/[websiteId]/export/route.ts @@ -1,9 +1,8 @@ import JSZip from 'jszip'; import Papa from 'papaparse'; -import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { dateRangeParams, pagingParams } from '@/lib/schema'; +import { pagingParams, withDateRange } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from '@/queries/sql'; @@ -11,8 +10,7 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const schema = z.object({ - ...dateRangeParams, + const schema = withDateRange({ ...pagingParams, }); diff --git a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts index d52c17736..34ebcd598 100644 --- a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { EVENT_COLUMNS, EVENT_TYPE, SESSION_COLUMNS } from '@/lib/constants'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { badRequest, json, unauthorized } from '@/lib/response'; -import { dateRangeParams, filterParams, searchParams } from '@/lib/schema'; +import { filterParams, searchParams, withDateRange } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getChannelExpandedMetrics, @@ -15,11 +15,10 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const schema = z.object({ + const schema = withDateRange({ type: z.string(), limit: z.coerce.number().optional(), offset: z.coerce.number().optional(), - ...dateRangeParams, ...searchParams, ...filterParams, }); diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index 12784adbe..c9649ae9a 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { EVENT_COLUMNS, EVENT_TYPE, SESSION_COLUMNS } from '@/lib/constants'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { badRequest, json, unauthorized } from '@/lib/response'; -import { dateRangeParams, filterParams, searchParams } from '@/lib/schema'; +import { filterParams, searchParams, withDateRange } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getChannelMetrics, @@ -15,11 +15,10 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const schema = z.object({ + const schema = withDateRange({ type: z.string(), limit: z.coerce.number().optional(), offset: z.coerce.number().optional(), - ...dateRangeParams, ...searchParams, ...filterParams, }); diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts index af59bce46..b9945c87b 100644 --- a/src/app/api/websites/[websiteId]/pageviews/route.ts +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -1,8 +1,7 @@ -import { z } from 'zod'; import { getCompareDate } from '@/lib/date'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { dateRangeParams, filterParams } from '@/lib/schema'; +import { filterParams, withDateRange } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getPageviewStats, getSessionStats } from '@/queries/sql'; @@ -10,8 +9,7 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const schema = z.object({ - ...dateRangeParams, + const schema = withDateRange({ ...filterParams, }); diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts index ed4757a1c..88199aff9 100644 --- a/src/app/api/websites/[websiteId]/sessions/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -1,7 +1,6 @@ -import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { dateRangeParams, filterParams, pagingParams, searchParams } from '@/lib/schema'; +import { filterParams, pagingParams, searchParams, withDateRange } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getWebsiteSessions } from '@/queries/sql'; @@ -9,8 +8,7 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const schema = z.object({ - ...dateRangeParams, + const schema = withDateRange({ ...filterParams, ...pagingParams, ...searchParams, diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts index 9d21f4f55..bb060e41f 100644 --- a/src/app/api/websites/[websiteId]/stats/route.ts +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -1,8 +1,7 @@ -import { z } from 'zod'; import { getCompareDate } from '@/lib/date'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { dateRangeParams, filterParams } from '@/lib/schema'; +import { filterParams, withDateRange } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getWebsiteStats } from '@/queries/sql'; @@ -10,8 +9,7 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const schema = z.object({ - ...dateRangeParams, + const schema = withDateRange({ ...filterParams, }); diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts index 172325e3f..f07e60f3d 100644 --- a/src/app/api/websites/[websiteId]/values/route.ts +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -1,8 +1,7 @@ -import { z } from 'zod'; import { EVENT_COLUMNS, FILTER_COLUMNS, SEGMENT_TYPES, SESSION_COLUMNS } from '@/lib/constants'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { badRequest, json, unauthorized } from '@/lib/response'; -import { dateRangeParams, fieldsParam, searchParams } from '@/lib/schema'; +import { fieldsParam, searchParams, withDateRange } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getWebsiteSegments } from '@/queries/prisma'; import { getValues } from '@/queries/sql'; @@ -11,9 +10,8 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const schema = z.object({ + const schema = withDateRange({ type: fieldsParam, - ...dateRangeParams, ...searchParams, }); diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 020ed0c25..197acea35 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -23,6 +23,25 @@ export const dateRangeParams = { compare: z.enum(['prev', 'yoy']).optional(), }; +export function withDateRange(shape?: T) { + return z + .object({ + ...dateRangeParams, + ...shape, + }) + .superRefine((data: Record, ctx) => { + const hasTimestamps = data.startAt != null && data.endAt != null; + const hasDates = data.startDate != null && data.endDate != null; + + if (!hasTimestamps && !hasDates) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Either startAt+endAt or startDate+endDate must be provided', + }); + } + }); +} + export const filterParams = { path: z.string().optional(), referrer: z.string().optional(), From 078f0827212866e566f3afca2504d32ceec55402 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 9 Feb 2026 17:48:28 -0800 Subject: [PATCH 4/4] do not pass in shareId in websiteEditForm --- .../(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx index 4ae819ee5..cbc88cd86 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx @@ -8,7 +8,8 @@ export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSa const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`); const handleSubmit = async (data: any) => { - await mutateAsync(data, { + const { shareId, ...updateData } = data; + await mutateAsync(updateData, { onSuccess: async () => { toast(formatMessage(messages.saved)); touch('websites');