diff --git a/src/app/(main)/websites/[websiteId]/(reports)/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/page.tsx deleted file mode 100644 index c9916b15..00000000 --- a/src/app/(main)/websites/[websiteId]/(reports)/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import GoalsPage from './goals/page'; - -export default GoalsPage; diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index cf1cae24..94022e87 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -32,7 +32,7 @@ export function WebsiteFilterButton({ {}, ); - const url = replaceParams({ ...params, segment: segment?.id }); + const url = replaceParams({ ...params, segment }); router.push(url); }; diff --git a/src/components/common/FilterEditForm.tsx b/src/components/common/FilterEditForm.tsx index 80574c5e..45c402d2 100644 --- a/src/components/common/FilterEditForm.tsx +++ b/src/components/common/FilterEditForm.tsx @@ -21,10 +21,11 @@ export function FilterEditForm({ }: FilterEditFormProps) { const { formatMessage, labels } = useMessages(); const [currentFilters, setCurrentFilters] = useState(filters); - const [currentSegment, setCurrentSegment] = useState(null); + const [currentSegment, setCurrentSegment] = useState(segmentId); const handleReset = () => { setCurrentFilters([]); + setCurrentSegment(null); }; const handleSave = () => { @@ -32,8 +33,8 @@ export function FilterEditForm({ onClose?.(); }; - const handleSegmentChange = (segment?: { id: string }) => { - setCurrentSegment(segment); + const handleSegmentChange = (id: string) => { + setCurrentSegment(id); }; return ( @@ -49,7 +50,7 @@ export function FilterEditForm({ diff --git a/src/components/hooks/queries/useWebsiteMetricsQuery.ts b/src/components/hooks/queries/useWebsiteMetricsQuery.ts index 17db2d92..f521798e 100644 --- a/src/components/hooks/queries/useWebsiteMetricsQuery.ts +++ b/src/components/hooks/queries/useWebsiteMetricsQuery.ts @@ -2,7 +2,6 @@ import { keepPreviousData } from '@tanstack/react-query'; import { useApi } from '../useApi'; import { useFilterParameters } from '../useFilterParameters'; import { useDateParameters } from '../useDateParameters'; -import { useSearchParams } from 'next/navigation'; import { ReactQueryOptions } from '@/lib/types'; export type WebsiteMetricsData = { @@ -18,7 +17,6 @@ export function useWebsiteMetricsQuery( const { get, useQuery } = useApi(); const date = useDateParameters(websiteId); const filters = useFilterParameters(); - const searchParams = useSearchParams(); return useQuery({ queryKey: [ @@ -34,7 +32,6 @@ export function useWebsiteMetricsQuery( get(`/websites/${websiteId}/metrics`, { ...date, ...filters, - [searchParams.get('view')]: undefined, ...params, }), enabled: !!websiteId, diff --git a/src/components/hooks/useFilterParameters.ts b/src/components/hooks/useFilterParameters.ts index 44c8896e..54032120 100644 --- a/src/components/hooks/useFilterParameters.ts +++ b/src/components/hooks/useFilterParameters.ts @@ -21,6 +21,8 @@ export function useFilterParameters() { page, pageSize, search, + segment, + cohort, }, } = useNavigation(); @@ -41,6 +43,8 @@ export function useFilterParameters() { tag, hostname, search, + segment, + cohort, }; }, [ path, @@ -60,5 +64,7 @@ export function useFilterParameters() { page, pageSize, search, + segment, + cohort, ]); } diff --git a/src/components/input/SegmentFilters.tsx b/src/components/input/SegmentFilters.tsx index 51690a01..6d1936ee 100644 --- a/src/components/input/SegmentFilters.tsx +++ b/src/components/input/SegmentFilters.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { List, Column, ListItem } from '@umami/react-zen'; import { useWebsiteSegmentsQuery } from '@/components/hooks'; import { LoadingPanel } from '@/components/common/LoadingPanel'; @@ -11,17 +10,15 @@ export interface SegmentFiltersProps { export function SegmentFilters({ websiteId, segmentId, onSave }: SegmentFiltersProps) { const { data, isLoading } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' }); - const [currentSegment, setCurrentSegment] = useState(segmentId); - const handleSave = (id: string) => { - setCurrentSegment(id); - onSave?.(data.find(item => item.id === id)); + const handleChange = (id: string) => { + onSave?.(id); }; return ( - handleSave(id[0])}> + handleChange(id[0])}> {data?.map(item => { return ( diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 1670546e..bb184d6b 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -87,21 +87,6 @@ function mapFilter(column: string, operator: string, name: string, type: string } } -function mapCohortFilter(column: string, operator: string, value: string) { - switch (operator) { - case OPERATORS.equals: - return `${column} = '${value}'`; - case OPERATORS.notEquals: - return `${column} != '${value}'`; - case OPERATORS.contains: - return `positionCaseInsensitive(${column}, '${value}') > 0`; - case OPERATORS.doesNotContain: - return `positionCaseInsensitive(${column}, '${value}') = 0`; - default: - return ''; - } -} - function getFilterQuery(filters: Record, options: QueryOptions = {}) { const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => { if (column) { @@ -118,40 +103,22 @@ function getFilterQuery(filters: Record, options: QueryOptions = {} return query.join('\n'); } -function getCohortQuery(websiteId: string, filters: QueryFilters = {}, options: QueryOptions = {}) { - const query = filtersToArray(filters, options).reduce( - (arr, { name, column, operator, value }) => { - if (column) { - arr.push( - `${arr.length === 0 ? 'where' : 'and'} ${mapCohortFilter(column, operator, value)}`, - ); - - if (name === 'referrer') { - arr.push(`and referrer_domain != hostname`); - } - } - - return arr; - }, - [], - ); - - if (query.length > 0) { - // add website and date range filters - query.push(`and website_id = '${websiteId}'`); - query.push( - `and created_at between parseDateTimeBestEffort('${filters.startDate}') and parseDateTimeBestEffort('${filters.endDate}')`, - ); - - return `join - (select distinct session_id - from website_event - ${query.join('\n')}) cohort - on cohort.session_id = website_event.session_id - `; +function getCohortQuery(filters: Record, options: QueryOptions = {}) { + if (!filters) { + return ''; } - return ''; + const filterQuery = getFilterQuery(filters, options); + + return `join ( + select distinct session_id + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + ) as cohort + on cohort.session_id = website_event.session_id + `; } function getDateQuery(filters: Record) { @@ -192,7 +159,7 @@ function parseFilters(filters: Record, options?: QueryOptions) { filterQuery: getFilterQuery(filters, options), dateQuery: getDateQuery(filters), queryParams: getQueryParams(filters), - cohortQuery: getCohortQuery(filters?.cohort), + cohortQuery: getCohortQuery(filters?.cohortFilters, options), }; } diff --git a/src/lib/params.ts b/src/lib/params.ts index e769ed61..9b3abf58 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -5,7 +5,7 @@ export function parseParameterValue(param: any) { if (typeof param === 'string') { const [, operator, value] = param.match(/^([a-z]+)\.(.*)/) || []; - return { operator, value }; + return { operator: operator || OPERATORS.equals, value: value || param }; } return { operator: OPERATORS.equals, value: param }; } diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index b8ad4984..ab97b7d6 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -241,7 +241,7 @@ async function pagedQuery(model: string, criteria: T, filters?: QueryFilters) return { data, count, page: +page, pageSize: size, orderBy, search }; } -async function pagedRawQuery( +async function rawPagedQuery( query: string, filters: QueryFilters, queryParams: Record, @@ -360,7 +360,7 @@ export default { getTimestampDiffSQL, getSearchSQL, pagedQuery, - pagedRawQuery, + pagedRawQuery: rawPagedQuery, parseFilters, rawQuery, }; diff --git a/src/lib/request.ts b/src/lib/request.ts index 3b16f355..c5e5b9bc 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,5 +1,5 @@ import { z } from 'zod/v4'; -import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE, FILTER_GROUPS } from '@/lib/constants'; +import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE } from '@/lib/constants'; import { badRequest, unauthorized } from '@/lib/response'; import { getAllowedUnits, getMinimumUnit, maxDate } from '@/lib/date'; import { checkAuth } from '@/lib/auth'; @@ -79,16 +79,6 @@ export function getRequestFilters(query: Record) { return result; } -export async function getRequestSegments(websiteId: string, query: Record) { - for (const key of Object.keys(FILTER_GROUPS)) { - const value = query[key]; - - if (value !== undefined) { - return getWebsiteSegment(websiteId, key, value); - } - } -} - export async function setWebsiteDate(websiteId: string, data: Record) { const website = await fetchWebsite(websiteId); @@ -109,7 +99,13 @@ export async function getQueryFilters( if (websiteId) { await setWebsiteDate(websiteId, dateRange); - Object.assign(filters, await getRequestSegments(websiteId, params)); + if (params.segment) { + Object.assign(filters, (await getWebsiteSegment(websiteId, params.segment))?.parameters); + } + + if (params.cohort) { + filters.cohortFilters = (await getWebsiteSegment(websiteId, params.cohort))?.parameters; + } } return { diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 41931b7b..7a655498 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -35,8 +35,8 @@ export const filterParams = { hostname: z.string().optional(), language: z.string().optional(), event: z.string().optional(), - segment: z.string().optional(), - cohort: z.string().optional(), + segment: z.string().uuid().optional(), + cohort: z.string().uuid().optional(), eventType: z.coerce.number().int().positive().optional(), }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 29d566a5..81627458 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -44,7 +44,14 @@ export interface QueryOptions { prefix?: string; } -export interface QueryFilters extends DateParams, FilterParams, SortParams, PageParams {} +export interface QueryFilters + extends DateParams, + FilterParams, + SortParams, + PageParams, + SegmentParams { + cohortFilters?: QueryFilters; +} export interface DateParams { startDate?: Date; @@ -86,6 +93,11 @@ export interface PageParams { pageSize?: number; } +export interface SegmentParams { + segment?: string; + cohort?: string; +} + export interface PageResult { data: T; count: number; diff --git a/src/queries/prisma/segment.ts b/src/queries/prisma/segment.ts index 1f962e8f..01b82e9b 100644 --- a/src/queries/prisma/segment.ts +++ b/src/queries/prisma/segment.ts @@ -13,13 +13,9 @@ export async function getSegment(segmentId: string): Promise { }); } -export async function getWebsiteSegment( - websiteId: string, - type: string, - name: string, -): Promise { - return prisma.client.segment.findFirst({ - where: { websiteId, type, name }, +export async function getWebsiteSegment(websiteId: string, segmentId: string): Promise { + return prisma.client.Segment.findFirst({ + where: { id: segmentId, websiteId }, }); }