diff --git a/src/app/(main)/settings/users/UsersDataTable.tsx b/src/app/(main)/settings/users/UsersDataTable.tsx index f4173a32..d0cc5ec8 100644 --- a/src/app/(main)/settings/users/UsersDataTable.tsx +++ b/src/app/(main)/settings/users/UsersDataTable.tsx @@ -3,17 +3,11 @@ import { useUsersQuery } from '@/components/hooks'; import { UsersTable } from './UsersTable'; import { ReactNode } from 'react'; -export function UsersDataTable({ - showActions, - children, -}: { - showActions?: boolean; - children?: ReactNode; -}) { +export function UsersDataTable({ showActions }: { showActions?: boolean; children?: ReactNode }) { const queryResult = useUsersQuery(); return ( - children}> + {({ data }) => } ); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx index c9487ba6..f173717c 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx @@ -19,7 +19,6 @@ import { TagsTable } from '@/components/metrics/TagsTable'; import { getCompareDate } from '@/lib/date'; import { formatNumber } from '@/lib/format'; import { useState } from 'react'; -import { useWebsites } from '@/store/websites'; import { Panel } from '@/components/common/Panel'; import { DateDisplay } from '@/components/common/DateDisplay'; @@ -42,8 +41,7 @@ const views = { export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { const [data, setData] = useState([]); - const { dateRange } = useDateRange(websiteId); - const dateCompare = useWebsites(state => state[websiteId]?.dateCompare); + const { dateRange, dateCompare } = useDateRange(websiteId); const { formatMessage, labels } = useMessages(); const { updateParams, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index d2ef3f4b..2abf448b 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -17,43 +17,43 @@ export function WebsiteMetricsBar({ const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId); const isAllTime = dateRange.value === 'all'; - const { pageviews, visitors, visits, bounces, totaltime, previous } = data || {}; + const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {}; const metrics = data ? [ { value: visitors, label: formatMessage(labels.visitors), - change: visitors - previous.visitors, + change: visitors - comparison.visitors, formatValue: formatLongNumber, }, { value: visits, label: formatMessage(labels.visits), - change: visits - previous.visits, + change: visits - comparison.visits, formatValue: formatLongNumber, }, { value: pageviews, label: formatMessage(labels.views), - change: pageviews - previous.pageviews, + change: pageviews - comparison.pageviews, formatValue: formatLongNumber, }, { label: formatMessage(labels.bounceRate), value: (Math.min(visits, bounces) / visits) * 100, - prev: (Math.min(previous.visits, previous.bounces) / previous.visits) * 100, + prev: (Math.min(comparison.visits, comparison.bounces) / comparison.visits) * 100, change: (Math.min(visits, bounces) / visits) * 100 - - (Math.min(previous.visits, previous.bounces) / previous.visits) * 100, + (Math.min(comparison.visits, comparison.bounces) / comparison.visits) * 100, formatValue: n => Math.round(+n) + '%', reverseColors: true, }, { label: formatMessage(labels.visitDuration), value: totaltime / visits, - prev: previous.totaltime / previous.visits, - change: totaltime / visits - previous.totaltime / previous.visits, + prev: comparison.totaltime / comparison.visits, + change: totaltime / visits - comparison.totaltime / comparison.visits, formatValue: n => `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`, }, diff --git a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx index d8c23476..70c0ca56 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx @@ -1,7 +1,9 @@ -import { useWebsiteEventsQuery } from '@/components/hooks'; +import { useState } from 'react'; +import { useMessages, useWebsiteEventsQuery } from '@/components/hooks'; import { EventsTable } from './EventsTable'; import { DataGrid } from '@/components/common/DataGrid'; import { ReactNode } from 'react'; +import { FilterButtons } from '@/components/common/FilterButtons'; export function EventsDataTable({ websiteId, @@ -10,10 +12,37 @@ export function EventsDataTable({ teamId?: string; children?: ReactNode; }) { + const { formatMessage, labels } = useMessages(); const queryResult = useWebsiteEventsQuery(websiteId); + const [view, setView] = useState('all'); + + const buttons = [ + { + id: 'all', + label: formatMessage(labels.all), + }, + { + id: 'page', + label: formatMessage(labels.page), + }, + { + id: 'event', + label: formatMessage(labels.event), + }, + ]; + + const renderActions = () => { + return ; + }; return ( - + {({ data }) => } ); diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx index 9a21f21d..598e3c7d 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx @@ -5,6 +5,7 @@ import { Avatar } from '@/components/common/Avatar'; import Link from 'next/link'; import { Bolt, Eye } from '@/components/icons'; import { DateDistance } from '@/components/common/DateDistance'; +import { TypeIcon } from '@/components/common/TypeIcon'; export function EventsTable({ data = [] }) { const { formatMessage, labels } = useMessages(); @@ -16,13 +17,6 @@ export function EventsTable({ data = [] }) { return ( - - {(row: any) => ( - - - - )} - {(row: any) => { return ( @@ -34,8 +28,21 @@ export function EventsTable({ data = [] }) { ); }} - - {(row: any) => } + + {(row: any) => ( + + + + + + + + + + + + + )} ); diff --git a/src/app/(main)/websites/[websiteId]/reports/attribution/Attribution.tsx b/src/app/(main)/websites/[websiteId]/reports/attribution/Attribution.tsx index 87ea170d..192d2c4f 100644 --- a/src/app/(main)/websites/[websiteId]/reports/attribution/Attribution.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/attribution/Attribution.tsx @@ -30,15 +30,11 @@ export function Attribution({ }: AttributionProps) { const { data, error, isLoading } = useResultQuery('attribution', { websiteId, - dateRange: { - startDate, - endDate, - }, - parameters: { - model, - type, - step, - }, + startDate, + endDate, + model, + type, + step, }); const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/reports/breakdown/Breakdown.tsx b/src/app/(main)/websites/[websiteId]/reports/breakdown/Breakdown.tsx index bc5e2111..404e7de2 100644 --- a/src/app/(main)/websites/[websiteId]/reports/breakdown/Breakdown.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/breakdown/Breakdown.tsx @@ -7,12 +7,10 @@ export interface BreakdownProps { websiteId: string; startDate: Date; endDate: Date; - parameters: { - fields: string[]; - }; + selectedFields: string[]; } -export function Breakdown({ websiteId, parameters, startDate, endDate }: BreakdownProps) { +export function Breakdown({ websiteId, selectedFields = [], startDate, endDate }: BreakdownProps) { const { formatMessage, labels } = useMessages(); const { formatValue } = useFormat(); const { fields } = useFields(); @@ -20,19 +18,17 @@ export function Breakdown({ websiteId, parameters, startDate, endDate }: Breakdo 'breakdown', { websiteId, - dateRange: { - startDate, - endDate, - }, - parameters, + startDate, + endDate, + fields: selectedFields, }, - { enabled: !!parameters.fields.length }, + { enabled: !!selectedFields.length }, ); return ( - {parameters?.fields.map(field => { + {selectedFields.map(field => { return ( f.name === field)?.label}> {row => { diff --git a/src/app/(main)/websites/[websiteId]/reports/breakdown/BreakdownPage.tsx b/src/app/(main)/websites/[websiteId]/reports/breakdown/BreakdownPage.tsx index 890c440b..ed6a503d 100644 --- a/src/app/(main)/websites/[websiteId]/reports/breakdown/BreakdownPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/breakdown/BreakdownPage.tsx @@ -23,7 +23,7 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) { websiteId={websiteId} startDate={startDate} endDate={endDate} - parameters={{ fields }} + selectedFields={fields} /> diff --git a/src/app/(main)/websites/[websiteId]/reports/funnels/Funnel.tsx b/src/app/(main)/websites/[websiteId]/reports/funnels/Funnel.tsx index d21548a5..526ad215 100644 --- a/src/app/(main)/websites/[websiteId]/reports/funnels/Funnel.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/funnels/Funnel.tsx @@ -17,15 +17,11 @@ type FunnelResult = { remaining: number; }; -export function Funnel({ id, name, type, parameters, websiteId, startDate, endDate }) { +export function Funnel({ id, name, type, parameters, websiteId }) { const { formatMessage, labels } = useMessages(); - const { data, error, isLoading } = useResultQuery(type, { + const { data, error, isLoading } = useResultQuery(type, { websiteId, - dateRange: { - startDate, - endDate, - }, - parameters, + ...parameters, }); return ( diff --git a/src/app/(main)/websites/[websiteId]/reports/funnels/FunnelsPage.tsx b/src/app/(main)/websites/[websiteId]/reports/funnels/FunnelsPage.tsx index a479baeb..1918aadf 100644 --- a/src/app/(main)/websites/[websiteId]/reports/funnels/FunnelsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/funnels/FunnelsPage.tsx @@ -9,7 +9,7 @@ import { LoadingPanel } from '@/components/common/LoadingPanel'; import { Panel } from '@/components/common/Panel'; export function FunnelsPage({ websiteId }: { websiteId: string }) { - const { result, query } = useReportsQuery({ websiteId, type: 'funnel' }); + const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' }); const { dateRange: { startDate, endDate }, } = useDateRange(websiteId); @@ -20,14 +20,16 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) { - - - {result?.data?.map((report: any) => ( - - - - ))} - + + {data && ( + + {data['data']?.map((report: any) => ( + + + + ))} + + )} ); diff --git a/src/app/(main)/websites/[websiteId]/reports/goals/Goal.tsx b/src/app/(main)/websites/[websiteId]/reports/goals/Goal.tsx index e1480491..31941936 100644 --- a/src/app/(main)/websites/[websiteId]/reports/goals/Goal.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/goals/Goal.tsx @@ -26,11 +26,9 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate const { formatMessage, labels } = useMessages(); const { data, error, isLoading, isFetching } = useResultQuery(type, { websiteId, - dateRange: { - startDate, - endDate, - }, - parameters, + startDate, + endDate, + ...parameters, }); const isPage = parameters?.type === 'page'; diff --git a/src/app/(main)/websites/[websiteId]/reports/journeys/Journey.tsx b/src/app/(main)/websites/[websiteId]/reports/journeys/Journey.tsx index e49392b1..965e7a31 100644 --- a/src/app/(main)/websites/[websiteId]/reports/journeys/Journey.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/journeys/Journey.tsx @@ -22,28 +22,15 @@ export interface JourneyProps { endStep?: string; } -export function Journey({ - websiteId, - startDate, - endDate, - steps, - startStep, - endStep, -}: JourneyProps) { +export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps) { const [selectedNode, setSelectedNode] = useState(null); const [activeNode, setActiveNode] = useState(null); const { formatMessage, labels } = useMessages(); const { data, error, isLoading } = useResultQuery('journey', { websiteId, - dateRange: { - startDate, - endDate, - }, - parameters: { - steps, - startStep, - endStep, - }, + steps, + startStep, + endStep, }); useEscapeKey(() => setSelectedNode(null)); diff --git a/src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx b/src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx index 5bd568b2..1ceae260 100644 --- a/src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; import { Grid, Row, Column, Text, Icon } from '@umami/react-zen'; import { Users } from '@/components/icons'; -import { useMessages, useLocale, useResultQuery, useTimezone } from '@/components/hooks'; +import { useMessages, useLocale, useResultQuery } from '@/components/hooks'; import { formatDate } from '@/lib/date'; import { formatLongNumber } from '@/lib/format'; import { Panel } from '@/components/common/Panel'; @@ -19,14 +19,10 @@ export interface RetentionProps { export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) { const { formatMessage, labels } = useMessages(); const { locale } = useLocale(); - const { timezone } = useTimezone(); - const { data, error, isLoading } = useResultQuery('retention', { + const { data, error, isLoading } = useResultQuery('retention', { websiteId, - dateRange: { - startDate, - endDate, - timezone, - }, + startDate, + endDate, }); const rows = diff --git a/src/app/(main)/websites/[websiteId]/reports/retention/RetentionPage.tsx b/src/app/(main)/websites/[websiteId]/reports/retention/RetentionPage.tsx index 64cf6823..64da8700 100644 --- a/src/app/(main)/websites/[websiteId]/reports/retention/RetentionPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/retention/RetentionPage.tsx @@ -8,7 +8,7 @@ import { endOfMonth, startOfMonth } from 'date-fns'; export function RetentionPage({ websiteId }: { websiteId: string }) { const { dateRange: { startDate }, - } = useDateRange(websiteId); + } = useDateRange(websiteId, { ignoreOffset: true }); const monthStartDate = startOfMonth(startDate); const monthEndDate = endOfMonth(startDate); diff --git a/src/app/(main)/websites/[websiteId]/reports/revenue/Revenue.tsx b/src/app/(main)/websites/[websiteId]/reports/revenue/Revenue.tsx index 2c39df24..5963c3c0 100644 --- a/src/app/(main)/websites/[websiteId]/reports/revenue/Revenue.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/revenue/Revenue.tsx @@ -32,13 +32,9 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) { const unit = getMinimumUnit(startDate, endDate); const { data, error, isLoading } = useResultQuery('revenue', { websiteId, - dateRange: { - startDate, - endDate, - }, - parameters: { - currency, - }, + startDate, + endDate, + currency, }); const renderCountryName = useCallback( diff --git a/src/app/(main)/websites/[websiteId]/reports/utm/UTM.tsx b/src/app/(main)/websites/[websiteId]/reports/utm/UTM.tsx index a1469de6..598831c4 100644 --- a/src/app/(main)/websites/[websiteId]/reports/utm/UTM.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/utm/UTM.tsx @@ -18,10 +18,8 @@ export function UTM({ websiteId, startDate, endDate }: UTMProps) { const { formatMessage, labels } = useMessages(); const { data, error, isLoading } = useResultQuery('utm', { websiteId, - dateRange: { - startDate, - endDate, - }, + startDate, + endDate, }); return ( diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx index 06323447..b9b8a6dc 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx @@ -1,21 +1,15 @@ import { useWebsiteSessionsQuery } from '@/components/hooks'; import { SessionsTable } from './SessionsTable'; import { DataGrid } from '@/components/common/DataGrid'; -import { ReactNode } from 'react'; -export function SessionsDataTable({ - websiteId, - children, -}: { - websiteId?: string; - teamId?: string; - children?: ReactNode; -}) { +export function SessionsDataTable({ websiteId }: { websiteId?: string; teamId?: string }) { const queryResult = useWebsiteSessionsQuery(websiteId); return ( - children} allowPaging> - {({ data }) => } + + {({ data }) => { + return ; + }} ); } diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx index 0b9ad43b..1d40e4ac 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx @@ -14,7 +14,7 @@ export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean {(row: any) => ( - + )} diff --git a/src/app/api/reports/attribution/route.ts b/src/app/api/reports/attribution/route.ts index e8384895..8c5ab9b3 100644 --- a/src/app/api/reports/attribution/route.ts +++ b/src/app/api/reports/attribution/route.ts @@ -1,8 +1,8 @@ import { canViewWebsite } from '@/lib/auth'; -import { parseRequest } from '@/lib/request'; +import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { reportResultSchema } from '@/lib/schema'; -import { getAttribution } from '@/queries/sql/reports/getAttribution'; +import { AttributionParameters, getAttribution } from '@/queries/sql/reports/getAttribution'; export async function POST(request: Request) { const { auth, body, error } = await parseRequest(request, reportResultSchema); @@ -11,26 +11,16 @@ export async function POST(request: Request) { return error(); } - const { - websiteId, - dateRange: { startDate, endDate }, - parameters: { model, type, step, currency }, - ...filters - } = body; + const { websiteId } = body; if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } - const data = await getAttribution(websiteId, { - ...filters, - startDate: new Date(startDate), - endDate: new Date(endDate), - model, - type, - step, - currency, - }); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = getQueryFilters(body.filters); + + const data = await getAttribution(websiteId, parameters as AttributionParameters, filters); return json(data); } diff --git a/src/app/api/reports/breakdown/route.ts b/src/app/api/reports/breakdown/route.ts index d97a552c..a95d22b2 100644 --- a/src/app/api/reports/breakdown/route.ts +++ b/src/app/api/reports/breakdown/route.ts @@ -1,7 +1,7 @@ import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; -import { parseRequest } from '@/lib/request'; -import { getBreakdown } from '@/queries'; +import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; +import { BreakdownParameters, getBreakdown } from '@/queries'; import { reportResultSchema } from '@/lib/schema'; export async function POST(request: Request) { @@ -11,22 +11,16 @@ export async function POST(request: Request) { return error(); } - const { - websiteId, - dateRange: { startDate, endDate }, - parameters: { fields }, - ...filters - } = body; + const { websiteId } = body; if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } - const data = await getBreakdown(websiteId, fields, { - ...filters, - startDate: new Date(startDate), - endDate: new Date(endDate), - }); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = getQueryFilters(body.filters); + + const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters); return json(data); } diff --git a/src/app/api/reports/funnel/route.ts b/src/app/api/reports/funnel/route.ts index 335776bf..db68b3c5 100644 --- a/src/app/api/reports/funnel/route.ts +++ b/src/app/api/reports/funnel/route.ts @@ -1,7 +1,7 @@ import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; -import { parseRequest } from '@/lib/request'; -import { getFunnel } from '@/queries'; +import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; +import { FunnelParameters, getFunnel } from '@/queries'; import { reportResultSchema } from '@/lib/schema'; export async function POST(request: Request) { @@ -11,24 +11,16 @@ export async function POST(request: Request) { return error(); } - const { - websiteId, - dateRange: { startDate, endDate }, - parameters: { steps, window }, - ...filters - } = body; + const { websiteId } = body; if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } - const data = await getFunnel(websiteId, { - ...filters, - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - windowMinutes: +window, - }); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = getQueryFilters(body.filters); + + const data = await getFunnel(websiteId, parameters as FunnelParameters, filters); return json(data); } diff --git a/src/app/api/reports/goal/route.ts b/src/app/api/reports/goal/route.ts index 69bd6ab2..f7d82549 100644 --- a/src/app/api/reports/goal/route.ts +++ b/src/app/api/reports/goal/route.ts @@ -1,7 +1,7 @@ import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; -import { getQueryFilters, parseRequest } from '@/lib/request'; -import { getGoal } from '@/queries/sql/reports/getGoal'; +import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; +import { getGoal, GoalParameters } from '@/queries/sql/reports/getGoal'; import { reportResultSchema } from '@/lib/schema'; export async function POST(request: Request) { @@ -11,27 +11,16 @@ export async function POST(request: Request) { return error(); } - const { - websiteId, - dateRange: { startDate, endDate }, - parameters: { type, value, property, operator }, - } = body; + const { websiteId } = body; if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } - const filters = await getQueryFilters(body.filters); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = getQueryFilters(body.filters); - const data = await getGoal(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - type, - value, - property, - operator, - filters, - }); + const data = await getGoal(websiteId, parameters as GoalParameters, filters); return json(data); } diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts index 20675bdf..3c751f1b 100644 --- a/src/app/api/reports/journey/route.ts +++ b/src/app/api/reports/journey/route.ts @@ -1,6 +1,6 @@ import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; -import { parseRequest } from '@/lib/request'; +import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { getJourney } from '@/queries'; import { reportResultSchema } from '@/lib/schema'; @@ -11,25 +11,15 @@ export async function POST(request: Request) { return error(); } - const { - websiteId, - dateRange: { startDate, endDate }, - parameters: { steps, startStep, endStep }, - ...filters - } = body; + const { websiteId, parameters, filters } = body; if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } - const data = await getJourney(websiteId, { - ...filters, - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - startStep, - endStep, - }); + const queryFilters = await setWebsiteDate(websiteId, getQueryFilters(filters)); + + const data = await getJourney(websiteId, parameters, queryFilters); return json(data); } diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts index b569e13a..dfdde298 100644 --- a/src/app/api/reports/retention/route.ts +++ b/src/app/api/reports/retention/route.ts @@ -1,7 +1,7 @@ import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; -import { parseRequest } from '@/lib/request'; -import { getRetention } from '@/queries'; +import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; +import { getRetention, RetentionParameters } from '@/queries'; import { reportResultSchema } from '@/lib/schema'; export async function POST(request: Request) { @@ -11,22 +11,16 @@ export async function POST(request: Request) { return error(); } - const { - websiteId, - dateRange: { startDate, endDate, timezone }, - ...filters - } = body; + const { websiteId } = body; if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } - const data = await getRetention(websiteId, { - ...filters, - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); + const filters = getQueryFilters(body.filters); + const parameters = await setWebsiteDate(websiteId, body.parameters); + + const data = await getRetention(websiteId, parameters as RetentionParameters, filters); return json(data); } diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts index ff4f4de4..c03b083b 100644 --- a/src/app/api/reports/revenue/route.ts +++ b/src/app/api/reports/revenue/route.ts @@ -1,8 +1,8 @@ import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; -import { parseRequest } from '@/lib/request'; +import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; import { reportResultSchema } from '@/lib/schema'; -import { getRevenue } from '@/queries/sql/reports/getRevenue'; +import { getRevenue, RevenuParameters } from '@/queries/sql/reports/getRevenue'; export async function POST(request: Request) { const { auth, body, error } = await parseRequest(request, reportResultSchema); @@ -11,24 +11,16 @@ export async function POST(request: Request) { return error(); } - const { - websiteId, - dateRange: { startDate, endDate, unit }, - parameters: { currency }, - ...filters - } = body; + const { websiteId } = body; if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } - const data = await getRevenue(websiteId, { - ...filters, - startDate: new Date(startDate), - endDate: new Date(endDate), - unit, - currency, - }); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = getQueryFilters(body.filters); + + const data = await getRevenue(websiteId, parameters as RevenuParameters, filters); return json(data); } diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts index a2845b25..039f88f2 100644 --- a/src/app/api/reports/utm/route.ts +++ b/src/app/api/reports/utm/route.ts @@ -1,7 +1,7 @@ import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; -import { parseRequest } from '@/lib/request'; -import { getUTM } from '@/queries'; +import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; +import { getUTM, UTMParameters } from '@/queries'; import { reportResultSchema } from '@/lib/schema'; export async function POST(request: Request) { @@ -11,21 +11,16 @@ export async function POST(request: Request) { return error(); } - const { - websiteId, - dateRange: { startDate, endDate }, - ...filters - } = body; + const { websiteId } = body; if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } - const data = await getUTM(websiteId, { - ...filters, - startDate: new Date(startDate), - endDate: new Date(endDate), - }); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = getQueryFilters(body.filters); + + const data = await getUTM(websiteId, parameters as UTMParameters, filters); return json(data); } diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts index 86f6dec8..0bf6f72c 100644 --- a/src/app/api/websites/[websiteId]/events/route.ts +++ b/src/app/api/websites/[websiteId]/events/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/lib/auth'; -import { pagingParams } from '@/lib/schema'; +import { dateRangeParams, pagingParams, filterParams } from '@/lib/schema'; import { getWebsiteEvents } from '@/queries'; export async function GET( @@ -10,8 +10,8 @@ export async function GET( { params }: { params: Promise<{ websiteId: string }> }, ) { const schema = z.object({ - startAt: z.coerce.number().int(), - endAt: z.coerce.number().int(), + ...dateRangeParams, + ...filterParams, ...pagingParams, }); diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts index 98d26c0d..f8d86cd5 100644 --- a/src/app/api/websites/[websiteId]/events/series/route.ts +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { parseRequest, getQueryFilters } from '@/lib/request'; +import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/lib/auth'; import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; @@ -29,7 +29,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters({ ...query, websiteId }); + const filters = await setWebsiteDate(websiteId, getQueryFilters(query)); const data = await getEventMetrics(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index 22fa5b30..4e6c321f 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -4,8 +4,6 @@ import { canViewWebsite } from '@/lib/auth'; import { SESSION_COLUMNS, EVENT_COLUMNS, - FILTER_COLUMNS, - OPERATORS, SEARCH_DOMAINS, SOCIAL_DOMAINS, EMAIL_DOMAINS, @@ -13,7 +11,7 @@ import { VIDEO_DOMAINS, PAID_AD_PARAMS, } from '@/lib/constants'; -import { parseRequest, getQueryFilters } from '@/lib/request'; +import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; import { json, unauthorized, badRequest } from '@/lib/response'; import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries'; import { filterParams } from '@/lib/schema'; @@ -45,20 +43,14 @@ export async function GET( return unauthorized(); } - const column = FILTER_COLUMNS[type] || type; - const filters = await getQueryFilters({ ...query, websiteId }); + const filters = await setWebsiteDate(websiteId, getQueryFilters(query)); if (search) { - filters[type] = { - name: type, - column, - operator: OPERATORS.contains, - value: search, - }; + filters[type] = `c.${search}`; } if (SESSION_COLUMNS.includes(type)) { - const data = await getSessionMetrics(websiteId, type, filters, limit, offset); + const data = await getSessionMetrics(websiteId, { type, limit, offset }, filters); if (type === 'language') { const combined = {}; @@ -80,7 +72,7 @@ export async function GET( } if (EVENT_COLUMNS.includes(type)) { - const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); + const data = await getPageviewMetrics(websiteId, { type, limit, offset }, filters); return json(data); } diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts index b25f1895..41cb1182 100644 --- a/src/app/api/websites/[websiteId]/pageviews/route.ts +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { canViewWebsite } from '@/lib/auth'; -import { getQueryFilters, parseRequest } from '@/lib/request'; +import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { dateRangeParams, filterParams } from '@/lib/schema'; import { getCompareDate } from '@/lib/date'; import { unauthorized, json } from '@/lib/response'; @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters({ ...query, websiteId }); + const filters = await setWebsiteDate(websiteId, getQueryFilters(query)); const [pageviews, sessions] = await Promise.all([ getPageviewStats(websiteId, filters), diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts index 5564b4d8..dd7502cd 100644 --- a/src/app/api/websites/[websiteId]/session-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -1,5 +1,5 @@ import { canViewWebsite } from '@/lib/auth'; -import { getQueryFilters, parseRequest } from '@/lib/request'; +import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { getSessionDataValues } from '@/queries'; import { z } from 'zod'; @@ -22,7 +22,7 @@ export async function GET( const { propertyName } = query; const { websiteId } = await params; - const filters = await getQueryFilters({ ...query, websiteId }); + const filters = await setWebsiteDate(websiteId, getQueryFilters(query)); if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts index c6afc638..dba227be 100644 --- a/src/app/api/websites/[websiteId]/sessions/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { getQueryFilters, parseRequest } from '@/lib/request'; +import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/lib/auth'; import { dateRangeParams, filterParams, pagingParams } from '@/lib/schema'; @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters({ ...query, websiteId }); + const filters = await setWebsiteDate(websiteId, getQueryFilters(query)); const data = await getWebsiteSessions(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts index a1746514..6bee799c 100644 --- a/src/app/api/websites/[websiteId]/sessions/stats/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { parseRequest, getQueryFilters } from '@/lib/request'; +import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/lib/auth'; import { filterParams } from '@/lib/schema'; @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query); + const filters = await setWebsiteDate(websiteId, getQueryFilters(query)); const metrics = await getWebsiteSessionStats(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts index 8e2dd3cb..bd85c96a 100644 --- a/src/app/api/websites/[websiteId]/stats/route.ts +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -1,9 +1,10 @@ import { z } from 'zod'; -import { parseRequest, getQueryFilters } from '@/lib/request'; +import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/lib/auth'; import { filterParams } from '@/lib/schema'; import { getWebsiteStats } from '@/queries'; +import { getCompareDate } from '@/lib/date'; export async function GET( request: Request, @@ -28,15 +29,17 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters({ ...query, websiteId }); + const filters = await setWebsiteDate(websiteId, getQueryFilters(query)); const data = await getWebsiteStats(websiteId, filters); - const previous = await getWebsiteStats(websiteId, { + const { startDate, endDate } = getCompareDate('prev', filters.startDate, filters.endDate); + + const comparison = await getWebsiteStats(websiteId, { ...filters, - startDate: filters.compareStartDate, - endDate: filters.compareEndDate, + startDate, + endDate, }); - return json({ ...data, previous }); + return json({ ...data, comparison }); } diff --git a/src/components/common/DataGrid.tsx b/src/components/common/DataGrid.tsx index 5b23b8f6..22344875 100644 --- a/src/components/common/DataGrid.tsx +++ b/src/components/common/DataGrid.tsx @@ -6,13 +6,13 @@ import { LoadingPanel } from '@/components/common/LoadingPanel'; const DEFAULT_SEARCH_DELAY = 600; -export interface DataTableProps { +export interface DataGridProps { queryResult: any; searchDelay?: number; allowSearch?: boolean; allowPaging?: boolean; autoFocus?: boolean; - renderEmpty?: () => ReactNode; + renderActions?: () => ReactNode; children: ReactNode | ((data: any) => ReactNode); } @@ -20,12 +20,13 @@ export function DataGrid({ queryResult, searchDelay = 600, allowSearch, - allowPaging, + allowPaging = true, autoFocus, + renderActions, children, -}: DataTableProps) { +}: DataGridProps) { const { formatMessage, labels } = useMessages(); - const { data, error, isLoading, isFetching, setParams } = queryResult || {}; + const { data, error, isLoading, isFetching, setParams } = queryResult; const { router, updateParams } = useNavigation(); const [search, setSearch] = useState(''); @@ -43,29 +44,33 @@ export function DataGrid({ return ( {allowSearch && (data || search) && ( - + + {renderActions?.()} )} - - {data ? (typeof children === 'function' ? children(data) : children) : null} - - {allowPaging && data && ( - - - + {data && ( + <> + {typeof children === 'function' ? children(data) : children} + {allowPaging && data && ( + + + + )} + )} diff --git a/src/components/hooks/queries/useEventDataEventsQuery.ts b/src/components/hooks/queries/useEventDataEventsQuery.ts index 73427b1d..41d34f74 100644 --- a/src/components/hooks/queries/useEventDataEventsQuery.ts +++ b/src/components/hooks/queries/useEventDataEventsQuery.ts @@ -1,14 +1,16 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); - const params = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:event-data:events', { websiteId, ...params }], - queryFn: () => get(`/websites/${websiteId}/event-data/events`, { ...params }), + queryKey: ['websites:event-data:events', { websiteId, ...date, ...filters }], + queryFn: () => get(`/websites/${websiteId}/event-data/events`, { ...date, ...filters }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useEventDataPropertiesQuery.ts b/src/components/hooks/queries/useEventDataPropertiesQuery.ts index 0e6735b6..31618940 100644 --- a/src/components/hooks/queries/useEventDataPropertiesQuery.ts +++ b/src/components/hooks/queries/useEventDataPropertiesQuery.ts @@ -1,14 +1,16 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); - const params = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:event-data:properties', { websiteId, ...params }], - queryFn: () => get(`/websites/${websiteId}/event-data/properties`, { ...params }), + queryKey: ['websites:event-data:properties', { websiteId, ...date, ...filters }], + queryFn: () => get(`/websites/${websiteId}/event-data/properties`, { ...date, ...filters }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useEventDataQuery.ts b/src/components/hooks/queries/useEventDataQuery.ts index 0901cdd1..05b095f3 100644 --- a/src/components/hooks/queries/useEventDataQuery.ts +++ b/src/components/hooks/queries/useEventDataQuery.ts @@ -1,5 +1,6 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; export function useEventDataQuery( @@ -8,11 +9,12 @@ export function useEventDataQuery( options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); - const params = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const params = useFilterParameters(); return useQuery({ - queryKey: ['websites:event-data', { websiteId, eventId, ...params }], - queryFn: () => get(`/websites/${websiteId}/event-data/${eventId}`, { ...params }), + queryKey: ['websites:event-data', { websiteId, eventId, ...date, ...params }], + queryFn: () => get(`/websites/${websiteId}/event-data/${eventId}`, { ...date, ...params }), enabled: !!(websiteId && eventId), ...options, }); diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useEventDataValuesQuery.ts index 6871214e..0bf87f86 100644 --- a/src/components/hooks/queries/useEventDataValuesQuery.ts +++ b/src/components/hooks/queries/useEventDataValuesQuery.ts @@ -1,5 +1,6 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; export function useEventDataValuesQuery( @@ -9,12 +10,21 @@ export function useEventDataValuesQuery( options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); - const params = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:event-data:values', { websiteId, eventName, propertyName, ...params }], + queryKey: [ + 'websites:event-data:values', + { websiteId, eventName, propertyName, ...date, ...filters }, + ], queryFn: () => - get(`/websites/${websiteId}/event-data/values`, { ...params, eventName, propertyName }), + get(`/websites/${websiteId}/event-data/values`, { + ...date, + ...filters, + eventName, + propertyName, + }), enabled: !!(websiteId && propertyName), ...options, }); diff --git a/src/components/hooks/queries/useResultQuery.ts b/src/components/hooks/queries/useResultQuery.ts index bbdf0dff..cbc5bf01 100644 --- a/src/components/hooks/queries/useResultQuery.ts +++ b/src/components/hooks/queries/useResultQuery.ts @@ -1,15 +1,17 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; import { ReactQueryOptions } from '@/lib/types'; +import { useDateParameters } from '@/components/hooks/useDateParameters'; export function useResultQuery( type: string, params?: Record, options?: ReactQueryOptions, ) { - const { websiteId } = params; + const { websiteId, ...parameters } = params; const { post, useQuery } = useApi(); - const filters = useFilterParams(websiteId); + const { startDate, endDate, timezone } = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ queryKey: [ @@ -17,11 +19,25 @@ export function useResultQuery( { type, websiteId, - ...filters, + startDate, + endDate, + timezone, ...params, + ...filters, }, ], - queryFn: () => post(`/reports/${type}`, { type, filters, ...params }), + queryFn: () => + post(`/reports/${type}`, { + websiteId, + type, + filters, + parameters: { + startDate, + endDate, + timezone, + ...parameters, + }, + }), enabled: !!type, ...options, }); diff --git a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts index 470f8a09..6c54638c 100644 --- a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts +++ b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts @@ -1,14 +1,16 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; export function useSessionDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); - const params = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:session-data:properties', { websiteId, ...params }], - queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { ...params }), + queryKey: ['websites:session-data:properties', { websiteId, ...date, ...filters }], + queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { ...date, ...filters }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useSessionDataValuesQuery.ts b/src/components/hooks/queries/useSessionDataValuesQuery.ts index e9b846e8..212d7628 100644 --- a/src/components/hooks/queries/useSessionDataValuesQuery.ts +++ b/src/components/hooks/queries/useSessionDataValuesQuery.ts @@ -1,5 +1,6 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; export function useSessionDataValuesQuery( @@ -8,11 +9,13 @@ export function useSessionDataValuesQuery( options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); - const params = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:session-data:values', { websiteId, propertyName, ...params }], - queryFn: () => get(`/websites/${websiteId}/session-data/values`, { ...params, propertyName }), + queryKey: ['websites:session-data:values', { websiteId, propertyName, ...date, ...filters }], + queryFn: () => + get(`/websites/${websiteId}/session-data/values`, { ...date, ...filters, propertyName }), enabled: !!(websiteId && propertyName), ...options, }); diff --git a/src/components/hooks/queries/useWebsiteEventsQuery.ts b/src/components/hooks/queries/useWebsiteEventsQuery.ts index a40f00bc..1ecac415 100644 --- a/src/components/hooks/queries/useWebsiteEventsQuery.ts +++ b/src/components/hooks/queries/useWebsiteEventsQuery.ts @@ -1,15 +1,17 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { usePagedQuery } from '../usePagedQuery'; import { ReactQueryOptions } from '@/lib/types'; export function useWebsiteEventsQuery(websiteId: string, options?: ReactQueryOptions) { const { get } = useApi(); - const queryParams = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return usePagedQuery({ - queryKey: ['websites:events', { websiteId, ...queryParams }], - queryFn: () => get(`/websites/${websiteId}/events`, { ...queryParams, pageSize: 20 }), + queryKey: ['websites:events', { websiteId, ...date, ...filters }], + queryFn: () => get(`/websites/${websiteId}/events`, { ...date, ...filters, pageSize: 20 }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts b/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts index c2b2a1e2..8d03b7ac 100644 --- a/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts +++ b/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts @@ -1,14 +1,16 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; export function useWebsiteEventsSeriesQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); - const params = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:events:series', { websiteId, ...params }], - queryFn: () => get(`/websites/${websiteId}/events/series`, { ...params }), + queryKey: ['websites:events:series', { websiteId, ...date, ...filters }], + queryFn: () => get(`/websites/${websiteId}/events/series`, { ...date, ...filters }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useWebsiteMetricsQuery.ts b/src/components/hooks/queries/useWebsiteMetricsQuery.ts index 2d6a6ef8..17db2d92 100644 --- a/src/components/hooks/queries/useWebsiteMetricsQuery.ts +++ b/src/components/hooks/queries/useWebsiteMetricsQuery.ts @@ -1,6 +1,7 @@ import { keepPreviousData } from '@tanstack/react-query'; import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { useSearchParams } from 'next/navigation'; import { ReactQueryOptions } from '@/lib/types'; @@ -11,11 +12,12 @@ export type WebsiteMetricsData = { export function useWebsiteMetricsQuery( websiteId: string, - params: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number }, + params: { type: string; limit?: number; search?: string }, options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); - const queryParams = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); const searchParams = useSearchParams(); return useQuery({ @@ -23,13 +25,15 @@ export function useWebsiteMetricsQuery( 'websites:metrics', { websiteId, - ...queryParams, + ...date, + ...filters, ...params, }, ], queryFn: async () => get(`/websites/${websiteId}/metrics`, { - ...queryParams, + ...date, + ...filters, [searchParams.get('view')]: undefined, ...params, }), diff --git a/src/components/hooks/queries/useWebsitePageviewsQuery.ts b/src/components/hooks/queries/useWebsitePageviewsQuery.ts index 63ec88ea..c2a12b0b 100644 --- a/src/components/hooks/queries/useWebsitePageviewsQuery.ts +++ b/src/components/hooks/queries/useWebsitePageviewsQuery.ts @@ -1,5 +1,6 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; export interface WebsitePageviewsData { @@ -12,11 +13,12 @@ export function useWebsitePageviewsQuery( options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); - const queryParams = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const queryParams = useFilterParameters(); return useQuery({ - queryKey: ['websites:pageviews', { websiteId, compare, ...queryParams }], - queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...queryParams }), + queryKey: ['websites:pageviews', { websiteId, compare, ...date, ...queryParams }], + queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...date, ...queryParams }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts b/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts index d60b3c77..8d1f5d55 100644 --- a/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts +++ b/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts @@ -1,13 +1,15 @@ import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; export function useWebsiteSessionStatsQuery(websiteId: string, options?: Record) { const { get, useQuery } = useApi(); - const params = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['sessions:stats', { websiteId, ...params }], - queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...params }), + queryKey: ['sessions:stats', { websiteId, ...date, ...filters }], + queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...date, ...filters }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/queries/useWebsiteSessionsQuery.ts b/src/components/hooks/queries/useWebsiteSessionsQuery.ts index 38405377..1d2fad67 100644 --- a/src/components/hooks/queries/useWebsiteSessionsQuery.ts +++ b/src/components/hooks/queries/useWebsiteSessionsQuery.ts @@ -1,7 +1,8 @@ import { useApi } from '../useApi'; import { usePagedQuery } from '../usePagedQuery'; import { useModified } from '../useModified'; -import { useFilterParams } from '@/components/hooks/useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '../useDateParameters'; export function useWebsiteSessionsQuery( websiteId: string, @@ -9,14 +10,16 @@ export function useWebsiteSessionsQuery( ) { const { get } = useApi(); const { modified } = useModified(`sessions`); - const filters = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return usePagedQuery({ - queryKey: ['sessions', { websiteId, modified, ...params, ...filters }], + queryKey: ['sessions', { websiteId, modified, ...params, ...date, ...filters }], queryFn: () => { return get(`/websites/${websiteId}/sessions`, { ...params, ...filters, + ...date, pageSize: 20, }); }, diff --git a/src/components/hooks/queries/useWebsiteSessionsWeeklyQuery.ts b/src/components/hooks/queries/useWebsiteSessionsWeeklyQuery.ts index ecfbde84..231ef393 100644 --- a/src/components/hooks/queries/useWebsiteSessionsWeeklyQuery.ts +++ b/src/components/hooks/queries/useWebsiteSessionsWeeklyQuery.ts @@ -1,6 +1,7 @@ import { useApi } from '../useApi'; import { useModified } from '../useModified'; -import { useFilterParams } from '@/components/hooks/useFilterParams'; +import { useDateParameters } from '../useDateParameters'; +import { useFilterParameters } from '@/components/hooks/useFilterParameters'; export function useWebsiteSessionsWeeklyQuery( websiteId: string, @@ -8,13 +9,15 @@ export function useWebsiteSessionsWeeklyQuery( ) { const { get, useQuery } = useApi(); const { modified } = useModified(`sessions`); - const filters = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['sessions', { websiteId, modified, ...params, ...filters }], + queryKey: ['sessions', { websiteId, modified, ...params, ...date, ...filters }], queryFn: () => { return get(`/websites/${websiteId}/sessions/weekly`, { ...params, + ...date, ...filters, }); }, diff --git a/src/components/hooks/queries/useWebsiteStatsQuery.ts b/src/components/hooks/queries/useWebsiteStatsQuery.ts index 4bf5f2d3..c42613a9 100644 --- a/src/components/hooks/queries/useWebsiteStatsQuery.ts +++ b/src/components/hooks/queries/useWebsiteStatsQuery.ts @@ -1,6 +1,7 @@ import { UseQueryOptions } from '@tanstack/react-query'; import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; +import { useFilterParameters } from '../useFilterParameters'; +import { useDateParameters } from '@/components/hooks/useDateParameters'; export interface WebsiteStatsData { pageviews: number; @@ -8,7 +9,7 @@ export interface WebsiteStatsData { visits: number; bounces: number; totaltime: number; - previous: { + comparison: { pageviews: number; visitors: number; visits: number; @@ -22,11 +23,12 @@ export function useWebsiteStatsQuery( options?: UseQueryOptions, ) { const { get, useQuery } = useApi(); - const filterParams = useFilterParams(websiteId); + const date = useDateParameters(websiteId); + const filters = useFilterParameters(); return useQuery({ - queryKey: ['websites:stats', { websiteId, ...filterParams }], - queryFn: () => get(`/websites/${websiteId}/stats`, { ...filterParams }), + queryKey: ['websites:stats', { websiteId, ...date, ...filters }], + queryFn: () => get(`/websites/${websiteId}/stats`, { ...date, ...filters }), enabled: !!websiteId, ...options, }); diff --git a/src/components/hooks/uesPageParameters.ts b/src/components/hooks/uesPageParameters.ts new file mode 100644 index 00000000..42cf3911 --- /dev/null +++ b/src/components/hooks/uesPageParameters.ts @@ -0,0 +1,16 @@ +import { useMemo } from 'react'; +import { useNavigation } from './useNavigation'; + +export function usePageParameters() { + const { + query: { page, pageSize, search }, + } = useNavigation(); + + return useMemo(() => { + return { + page, + pageSize, + search, + }; + }, [page, pageSize, search]); +} diff --git a/src/components/hooks/useDateParameters.ts b/src/components/hooks/useDateParameters.ts new file mode 100644 index 00000000..ed4a3e40 --- /dev/null +++ b/src/components/hooks/useDateParameters.ts @@ -0,0 +1,18 @@ +import { useDateRange } from './useDateRange'; +import { useTimezone } from './useTimezone'; + +export function useDateParameters(websiteId: string) { + const { + dateRange: { startDate, endDate, unit }, + } = useDateRange(websiteId); + const { timezone, toUtc } = useTimezone(); + + return { + startAt: +toUtc(startDate), + endAt: +toUtc(endDate), + startDate: toUtc(startDate).toISOString(), + endDate: toUtc(endDate).toISOString(), + unit, + timezone, + }; +} diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 795c4239..22abf825 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -8,7 +8,11 @@ import { useApi } from './useApi'; import { useNavigation } from './useNavigation'; import { useMemo } from 'react'; -export function useDateRange(websiteId?: string) { +export interface UseDateRangeOptions { + ignoreOffset?: boolean; +} + +export function useDateRange(websiteId?: string, options: UseDateRangeOptions = {}) { const { get } = useApi(); const { locale } = useLocale(); const { @@ -16,14 +20,16 @@ export function useDateRange(websiteId?: string) { } = useNavigation(); const websiteConfig = useWebsites(state => state[websiteId]?.dateRange); const globalConfig = useApp(state => state.dateRangeValue); - const dateRangeObject = parseDateRange( - date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE, - locale, - ); - const dateRange = useMemo( - () => (offset ? getOffsetDateRange(dateRangeObject, +offset) : dateRangeObject), - [date, offset, websiteConfig], - ); + const dateValue = date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE; + + const dateRange = useMemo(() => { + const dateRangeObject = parseDateRange(dateValue, locale); + + return !options.ignoreOffset && offset + ? getOffsetDateRange(dateRangeObject, +offset) + : dateRangeObject; + }, [date, offset, dateValue, options]); + const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE); const saveDateRange = async (value: string) => { diff --git a/src/components/hooks/useFilterParams.ts b/src/components/hooks/useFilterParameters.ts similarity index 54% rename from src/components/hooks/useFilterParams.ts rename to src/components/hooks/useFilterParameters.ts index d0b84f95..44c8896e 100644 --- a/src/components/hooks/useFilterParams.ts +++ b/src/components/hooks/useFilterParameters.ts @@ -1,11 +1,7 @@ +import { useMemo } from 'react'; import { useNavigation } from './useNavigation'; -import { useDateRange } from './useDateRange'; -import { useTimezone } from './useTimezone'; -export function useFilterParams(websiteId: string) { - const { dateRange } = useDateRange(websiteId); - const { startDate, endDate, unit } = dateRange; - const { timezone, toUtc } = useTimezone(); +export function useFilterParameters() { const { query: { path, @@ -28,13 +24,25 @@ export function useFilterParams(websiteId: string) { }, } = useNavigation(); - return { - // Date range - startAt: +toUtc(startDate), - endAt: +toUtc(endDate), - unit, - timezone, - // Filters + return useMemo(() => { + return { + path, + referrer, + title, + query, + host, + os, + browser, + device, + country, + region, + city, + event, + tag, + hostname, + search, + }; + }, [ path, referrer, title, @@ -49,9 +57,8 @@ export function useFilterParams(websiteId: string) { event, tag, hostname, - // Paging page, pageSize, search, - }; + ]); } diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index ebed8f82..45b6c9c5 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -46,7 +46,12 @@ export function MetricsTable({ const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery( websiteId, - { type, limit, search: searchFormattedValues ? undefined : search, ...params }, + { + type, + limit, + search: searchFormattedValues ? undefined : search, + ...params, + }, { retryDelay: delay || DEFAULT_ANIMATION_DURATION, }, diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 48cd4ce0..7f098e08 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -136,7 +136,7 @@ function getQueryParams(filters: Record) { }; } -async function parseFilters(filters: Record, options?: QueryOptions) { +function parseFilters(filters: Record, options?: QueryOptions) { return { filterQuery: getFilterQuery(filters, options), dateQuery: getDateQuery(filters), diff --git a/src/lib/kafka.ts b/src/lib/kafka.ts index 2287f9a7..a81f3d4b 100644 --- a/src/lib/kafka.ts +++ b/src/lib/kafka.ts @@ -16,7 +16,8 @@ const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER); function getClient() { const { username, password } = new URL(process.env.KAFKA_URL); const brokers = process.env.KAFKA_BROKER.split(','); - const mechanism = process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512'; + const mechanism = + (process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512') || 'plain'; const ssl: { ssl?: tls.ConnectionOptions | boolean; sasl?: SASLOptions } = username && password diff --git a/src/lib/params.ts b/src/lib/params.ts index b3d8ef0f..e769ed61 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -37,6 +37,7 @@ export function filtersToArray(filters: QueryFilters, options: QueryOptions = {} column: options?.columns?.[key] ?? FILTER_COLUMNS[key], operator, value, + prefix: options?.prefix, }); }, []); } diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index a024d93d..90ad2d92 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -84,15 +84,10 @@ function getTimestampDiffSQL(field1: string, field2: string): string { } function getSearchSQL(column: string, param: string = 'search'): string { - const db = getDatabaseType(); - const like = db === POSTGRESQL ? 'ilike' : 'like'; - - return `and ${column} ${like} {{${param}}}`; + return `and ${column} ilike {{${param}}}`; } function mapFilter(column: string, operator: string, name: string, type: string = '') { - const db = getDatabaseType(); - const like = db === POSTGRESQL ? 'ilike' : 'like'; const value = `{{${name}${type ? `::${type}` : ''}}}`; switch (operator) { @@ -101,28 +96,31 @@ function mapFilter(column: string, operator: string, name: string, type: string case OPERATORS.notEquals: return `${column} != ${value}`; case OPERATORS.contains: - return `${column} ${like} ${value}`; + return `${column} ilike ${value}`; case OPERATORS.doesNotContain: - return `${column} not ${like} ${value}`; + return `${column} not ilike ${value}`; default: return ''; } } function getFilterQuery(filters: Record, options: QueryOptions = {}): string { - const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => { - if (column) { - arr.push(`and ${mapFilter(column, operator, name)}`); + const query = filtersToArray(filters, options).reduce( + (arr, { name, column, operator, prefix = '' }) => { + if (column) { + arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name)}`); - if (name === 'referrer') { - arr.push( - `and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`, - ); + if (name === 'referrer') { + arr.push( + `and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`, + ); + } } - } - return arr; - }, []); + return arr; + }, + [], + ); return query.join('\n'); } @@ -154,7 +152,7 @@ function getQueryParams(filters: Record) { }; } -async function parseFilters(filters: Record, options?: QueryOptions) { +function parseFilters(filters: Record, options?: QueryOptions) { const joinSession = Object.keys(filters).find(key => ['referrer', ...SESSION_COLUMNS].includes(key), ); diff --git a/src/lib/request.ts b/src/lib/request.ts index 96c051ec..ef5d1d90 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,7 +1,7 @@ import { z } from 'zod/v4'; import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE } from '@/lib/constants'; import { badRequest, unauthorized } from '@/lib/response'; -import { getAllowedUnits, getCompareDate, getMinimumUnit, maxDate } from '@/lib/date'; +import { getAllowedUnits, getMinimumUnit, maxDate } from '@/lib/date'; import { checkAuth } from '@/lib/auth'; import { fetchWebsite } from '@/lib/load'; import { QueryFilters } from '@/lib/types'; @@ -50,23 +50,14 @@ export async function getJsonBody(request: Request) { } export function getRequestDateRange(query: Record) { - const { startAt, endAt, unit, compare, timezone } = query; + const { startAt, endAt, unit, timezone } = query; const startDate = new Date(+startAt); const endDate = new Date(+endAt); - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - return { startDate, endDate, - compare, - compareStartDate, - compareEndDate, timezone, unit: getAllowedUnits(startDate, endDate).includes(unit) ? unit @@ -86,11 +77,21 @@ export function getRequestFilters(query: Record) { }, {}); } -export async function getQueryFilters(params: Record): Promise { +export async function setWebsiteDate(websiteId: string, data: Record) { + const website = await fetchWebsite(websiteId); + + if (website) { + data.startDate = maxDate(data.startDate, new Date(website?.resetAt)); + } + + return data; +} + +export function getQueryFilters(params: Record): QueryFilters { const dateRange = getRequestDateRange(params); const filters = getRequestFilters(params); - const data = { + return { ...dateRange, ...filters, page: params?.page, @@ -98,17 +99,5 @@ export async function getQueryFilters(params: Record): Promise UNIT_TYPES.includes(value), }); export const dateRangeParams = { - startAt: z.coerce.number(), - endAt: z.coerce.number(), + startAt: z.coerce.number().optional(), + endAt: z.coerce.number().optional(), + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().optional(), timezone: timezoneParam.optional(), unit: unitParam.optional(), compare: z.string().optional(), @@ -33,12 +35,12 @@ export const filterParams = { hostname: z.string().optional(), language: z.string().optional(), event: z.string().optional(), + search: z.string().optional(), }; export const pagingParams = { page: z.coerce.number().int().positive().optional(), pageSize: z.coerce.number().int().positive().optional(), - search: z.string().optional(), }; export const sortingParams = { @@ -93,23 +95,63 @@ export const reportTypeParam = z.enum([ 'utm', ]); -export const reportParms = { - websiteId: z.string().uuid(), - dateRange: z.object({ - startDate: z.coerce.date(), - endDate: z.coerce.date(), - timezone: timezoneParam.optional(), - unit: unitParam.optional(), - compare: z.string().optional(), - compareStartDate: z.coerce.date().optional(), - compareEndDate: z.coerce.date().optional(), - }), -}; +export const dateRangeSchema = z.object({ ...dateRangeParams }).superRefine((data, ctx) => { + const hasTimestamps = data.startAt !== undefined && data.endAt !== undefined; + const hasDates = data.startDate !== undefined && data.endDate !== undefined; + + if (!hasTimestamps && !hasDates) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'You must provide either startAt & endAt or startDate & endDate.', + }); + } + + if (hasTimestamps && hasDates) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Provide either startAt & endAt or startDate & endDate, not both.', + }); + } + + if (data.startAt !== undefined && data.endAt === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'If you provide startAt, you must also provide endAt.', + path: ['endAt'], + }); + } + + if (data.endAt !== undefined && data.startAt === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'If you provide endAt, you must also provide startAt.', + path: ['startAt'], + }); + } + + if (data.startDate !== undefined && data.endDate === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'If you provide startDate, you must also provide endDate.', + path: ['endDate'], + }); + } + + if (data.endDate !== undefined && data.startDate === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'If you provide endDate, you must also provide startDate.', + path: ['startDate'], + }); + } +}); export const goalReportSchema = z.object({ type: z.literal('goal'), parameters: z .object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), type: z.string(), value: z.string(), operator: z.enum(['count', 'sum', 'average']).optional(), @@ -126,6 +168,8 @@ export const goalReportSchema = z.object({ export const funnelReportSchema = z.object({ type: z.literal('funnel'), parameters: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), window: z.coerce.number().positive(), steps: z .array( @@ -142,6 +186,8 @@ export const funnelReportSchema = z.object({ export const journeyReportSchema = z.object({ type: z.literal('journey'), parameters: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), steps: z.coerce.number().min(2).max(7), startStep: z.string().optional(), endStep: z.string().optional(), @@ -150,15 +196,27 @@ export const journeyReportSchema = z.object({ export const retentionReportSchema = z.object({ type: z.literal('retention'), + parameters: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), + timezone: z.string().optional(), + }), }); export const utmReportSchema = z.object({ type: z.literal('utm'), + parameters: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), + }), }); export const revenueReportSchema = z.object({ type: z.literal('revenue'), parameters: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), + timezone: z.string().optional(), currency: z.string(), }), }); @@ -166,6 +224,8 @@ export const revenueReportSchema = z.object({ export const attributionReportSchema = z.object({ type: z.literal('attribution'), parameters: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), model: z.enum(['first-click', 'last-click']), type: z.enum(['page', 'event']), step: z.string(), @@ -176,6 +236,8 @@ export const attributionReportSchema = z.object({ export const breakdownReportSchema = z.object({ type: z.literal('breakdown'), parameters: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), fields: z.array(fieldsParam), }), }); @@ -202,7 +264,7 @@ export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema); export const reportResultSchema = z.intersection( z.object({ - ...reportParms, + websiteId: z.string().uuid(), filters: z.object({ ...filterParams }), }), reportTypeSchema, diff --git a/src/lib/types.ts b/src/lib/types.ts index 25ad9104..e1263826 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -41,19 +41,20 @@ export interface QueryOptions { joinSession?: boolean; columns?: Record; limit?: number; + prefix?: string; } -export interface QueryFilters { - websiteId?: string; - // Date range +export interface QueryFilters extends DateParams, FilterParams, SortParams, PageParams {} + +export interface DateParams { startDate?: Date; endDate?: Date; - compareStartDate?: Date; - compareEndDate?: Date; - compare?: string; unit?: string; timezone?: string; - // Filters + compareDate?: Date; +} + +export interface FilterParams { path?: string; referrer?: string; title?: string; @@ -70,20 +71,16 @@ export interface QueryFilters { search?: string; tag?: string; eventType?: number; - // Paging - page?: number; - pageSize?: number; - // Sorting +} + +export interface SortParams { orderBy?: string; sortDescending?: boolean; } export interface PageParams { - page: number; - pageSize: number; - orderBy?: string; - sortDescending?: boolean; - search?: string; + page?: number; + pageSize?: number; } export interface PageResult { diff --git a/src/queries/sql/events/getEventDataEvents.ts b/src/queries/sql/events/getEventDataEvents.ts index 96120a3c..ac9f26d6 100644 --- a/src/queries/sql/events/getEventDataEvents.ts +++ b/src/queries/sql/events/getEventDataEvents.ts @@ -23,7 +23,7 @@ export async function getEventDataEvents( async function relationalQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters } = prisma; const { event } = filters; - const { queryParams } = await parseFilters(filters); + const { queryParams } = parseFilters(filters); if (event) { return rawQuery( @@ -73,7 +73,7 @@ async function clickhouseQuery( ): Promise<{ eventName: string; propertyName: string; dataType: number; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; const { event } = filters; - const { queryParams } = await parseFilters(filters); + const { queryParams } = parseFilters(filters); if (event) { return rawQuery( diff --git a/src/queries/sql/events/getEventDataFields.ts b/src/queries/sql/events/getEventDataFields.ts index 905acc84..8f3a88c3 100644 --- a/src/queries/sql/events/getEventDataFields.ts +++ b/src/queries/sql/events/getEventDataFields.ts @@ -12,7 +12,7 @@ export async function getEventDataFields(...args: [websiteId: string, filters: Q async function relationalQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters, getDateSQL } = prisma; - const { filterQuery, queryParams } = await parseFilters(filters); + const { filterQuery, queryParams } = parseFilters(filters); return rawQuery( ` @@ -43,7 +43,7 @@ async function clickhouseQuery( filters: QueryFilters, ): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters(filters); + const { filterQuery, queryParams } = parseFilters(filters); return rawQuery( ` diff --git a/src/queries/sql/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts index 9c138d66..9577b40c 100644 --- a/src/queries/sql/events/getEventDataProperties.ts +++ b/src/queries/sql/events/getEventDataProperties.ts @@ -17,7 +17,7 @@ async function relationalQuery( filters: QueryFilters & { propertyName?: string }, ) { const { rawQuery, parseFilters } = prisma; - const { filterQuery, queryParams } = await parseFilters(filters, { + const { filterQuery, queryParams } = parseFilters(filters, { columns: { propertyName: 'data_key' }, }); @@ -45,7 +45,7 @@ async function clickhouseQuery( filters: QueryFilters & { propertyName?: string }, ): Promise<{ eventName: string; propertyName: string; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters(filters, { + const { filterQuery, queryParams } = parseFilters(filters, { columns: { propertyName: 'data_key' }, }); diff --git a/src/queries/sql/events/getEventDataStats.ts b/src/queries/sql/events/getEventDataStats.ts index 1c2c5e36..a597096a 100644 --- a/src/queries/sql/events/getEventDataStats.ts +++ b/src/queries/sql/events/getEventDataStats.ts @@ -18,7 +18,7 @@ export async function getEventDataStats( async function relationalQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters } = prisma; - const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId }); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId }); return rawQuery( ` @@ -47,7 +47,7 @@ async function clickhouseQuery( filters: QueryFilters, ): Promise<{ events: number; properties: number; records: number }[]> { const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId }); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId }); return rawQuery( ` diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts index 4bbddfc3..3ccf53a0 100644 --- a/src/queries/sql/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -25,7 +25,7 @@ async function relationalQuery( filters: QueryFilters & { eventName?: string; propertyName?: string }, ) { const { rawQuery, parseFilters, getDateSQL } = prisma; - const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId }); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId }); return rawQuery( ` @@ -56,7 +56,7 @@ async function clickhouseQuery( filters: QueryFilters & { eventName?: string; propertyName?: string }, ): Promise<{ value: string; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId }); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId }); return rawQuery( ` diff --git a/src/queries/sql/events/getEventMetrics.ts b/src/queries/sql/events/getEventMetrics.ts index 6a3bff64..0f1d4a44 100644 --- a/src/queries/sql/events/getEventMetrics.ts +++ b/src/queries/sql/events/getEventMetrics.ts @@ -22,8 +22,9 @@ export async function getEventMetrics( async function relationalQuery(websiteId: string, filters: QueryFilters) { const { timezone = 'utc', unit = 'day' } = filters; const { rawQuery, getDateSQL, parseFilters } = prisma; - const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({ + const { filterQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, + websiteId, eventType: EVENT_TYPE.customEvent, }); @@ -49,11 +50,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery( websiteId: string, filters: QueryFilters, -): Promise<{ x: string; t: string; y: number }[]> { +): Promise { const { timezone = 'UTC', unit = 'day' } = filters; const { rawQuery, getDateSQL, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ + const { filterQuery, queryParams } = parseFilters({ ...filters, + websiteId, eventType: EVENT_TYPE.customEvent, }); diff --git a/src/queries/sql/events/getWebsiteEvents.ts b/src/queries/sql/events/getWebsiteEvents.ts index 507f68e1..284f9335 100644 --- a/src/queries/sql/events/getWebsiteEvents.ts +++ b/src/queries/sql/events/getWebsiteEvents.ts @@ -13,7 +13,7 @@ export function getWebsiteEvents(...args: [websiteId: string, filters: QueryFilt async function relationalQuery(websiteId: string, filters: QueryFilters) { const { pagedRawQuery, parseFilters } = prisma; const { search } = filters; - const { filterQuery, queryParams } = await parseFilters({ + const { filterQuery, dateQuery, queryParams } = parseFilters({ ...filters, websiteId, }); @@ -40,7 +40,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { event_name as "eventName" from website_event where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} + ${dateQuery} ${filterQuery} ${searchQuery} order by created_at desc @@ -52,7 +52,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) { const { pagedRawQuery, parseFilters } = clickhouse; - const { queryParams, dateQuery, filterQuery } = await parseFilters({ + const { queryParams, dateQuery, filterQuery } = parseFilters({ ...filters, websiteId, }); @@ -74,6 +74,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { referrer_path as referrerPath, referrer_query as referrerQuery, referrer_domain as referrerDomain, + country as country, + device as device, + os as os, + browser as browser, page_title as pageTitle, event_type as eventType, event_name as eventName diff --git a/src/queries/sql/getChannelMetrics.ts b/src/queries/sql/getChannelMetrics.ts index cbb7d1a3..57722bf3 100644 --- a/src/queries/sql/getChannelMetrics.ts +++ b/src/queries/sql/getChannelMetrics.ts @@ -12,7 +12,7 @@ export async function getChannelMetrics(...args: [websiteId: string, filters?: Q async function relationalQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters } = prisma; - const { queryParams, filterQuery, dateQuery } = await parseFilters(filters); + const { queryParams, filterQuery, dateQuery } = parseFilters(filters); return rawQuery( ` @@ -36,7 +36,7 @@ async function clickhouseQuery( filters: QueryFilters, ): Promise<{ x: string; y: number }[]> { const { rawQuery, parseFilters } = clickhouse; - const { queryParams, filterQuery, dateQuery } = await parseFilters(filters); + const { queryParams, filterQuery, dateQuery } = parseFilters(filters); const sql = ` select diff --git a/src/queries/sql/getRealtimeActivity.ts b/src/queries/sql/getRealtimeActivity.ts index 0544219d..65192672 100644 --- a/src/queries/sql/getRealtimeActivity.ts +++ b/src/queries/sql/getRealtimeActivity.ts @@ -12,7 +12,7 @@ export async function getRealtimeActivity(...args: [websiteId: string, filters: async function relationalQuery(websiteId: string, filters: QueryFilters) { const { rawQuery, parseFilters } = prisma; - const { queryParams, filterQuery, dateQuery } = await parseFilters(filters); + const { queryParams, filterQuery, dateQuery } = parseFilters(filters); return rawQuery( ` @@ -41,7 +41,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> { const { rawQuery, parseFilters } = clickhouse; - const { queryParams, filterQuery, dateQuery } = await parseFilters(filters); + const { queryParams, filterQuery, dateQuery } = parseFilters(filters); return rawQuery( ` diff --git a/src/queries/sql/getWebsiteDateRange.ts b/src/queries/sql/getWebsiteDateRange.ts index 038d3ffb..4c0d21f0 100644 --- a/src/queries/sql/getWebsiteDateRange.ts +++ b/src/queries/sql/getWebsiteDateRange.ts @@ -12,7 +12,7 @@ export async function getWebsiteDateRange(...args: [websiteId: string]) { async function relationalQuery(websiteId: string) { const { rawQuery, parseFilters } = prisma; - const { queryParams } = await parseFilters({ + const { queryParams } = parseFilters({ startDate: new Date(DEFAULT_RESET_DATE), websiteId, }); @@ -34,7 +34,7 @@ async function relationalQuery(websiteId: string) { async function clickhouseQuery(websiteId: string) { const { rawQuery, parseFilters } = clickhouse; - const { queryParams } = await parseFilters({ + const { queryParams } = parseFilters({ startDate: new Date(DEFAULT_RESET_DATE), websiteId, }); diff --git a/src/queries/sql/getWebsiteStats.ts b/src/queries/sql/getWebsiteStats.ts index 186e4674..77d88dbc 100644 --- a/src/queries/sql/getWebsiteStats.ts +++ b/src/queries/sql/getWebsiteStats.ts @@ -27,7 +27,7 @@ async function relationalQuery( filters: QueryFilters, ): Promise { const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma; - const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({ + const { filterQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, eventType: EVENT_TYPE.pageView, @@ -66,7 +66,7 @@ async function clickhouseQuery( filters: QueryFilters, ): Promise { const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, eventType: EVENT_TYPE.pageView, diff --git a/src/queries/sql/pageviews/getPageviewMetrics.ts b/src/queries/sql/pageviews/getPageviewMetrics.ts index d92720c8..86f8bc0c 100644 --- a/src/queries/sql/pageviews/getPageviewMetrics.ts +++ b/src/queries/sql/pageviews/getPageviewMetrics.ts @@ -4,14 +4,19 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import { QueryFilters } from '@/lib/types'; +export interface PageviewMetricsParameters { + type: string; + limit?: number | string; + offset?: number | string; +} + +export interface PageviewMetricsData { + x: string; + y: number; +} + export async function getPageviewMetrics( - ...args: [ - websiteId: string, - type: string, - filters: QueryFilters, - limit?: number | string, - offset?: number | string, - ] + ...args: [websiteId: string, parameters: PageviewMetricsParameters, filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -21,16 +26,16 @@ export async function getPageviewMetrics( async function relationalQuery( websiteId: string, - type: string, + parameters: PageviewMetricsParameters, filters: QueryFilters, - limit: number | string = 500, - offset: number | string = 0, -) { +): Promise { + const { type, limit = 500, offset = 0 } = parameters; const column = FILTER_COLUMNS[type] || type; const { rawQuery, parseFilters } = prisma; - const { filterQuery, joinSessionQuery, queryParams } = await parseFilters( + const { filterQuery, joinSessionQuery, queryParams } = parseFilters( { ...filters, + websiteId, eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, }, { @@ -87,15 +92,15 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - type: string, + parameters: PageviewMetricsParameters, filters: QueryFilters, - limit: number | string = 500, - offset: number | string = 0, -): Promise<{ x: string; y: number }[]> { +): Promise { + const { type, limit = 500, offset = 0 } = parameters; const column = FILTER_COLUMNS[type] || type; const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ + const { filterQuery, queryParams } = parseFilters({ ...filters, + websiteId, eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, }); diff --git a/src/queries/sql/pageviews/getPageviewStats.ts b/src/queries/sql/pageviews/getPageviewStats.ts index 6b03bf67..953352d7 100644 --- a/src/queries/sql/pageviews/getPageviewStats.ts +++ b/src/queries/sql/pageviews/getPageviewStats.ts @@ -14,7 +14,7 @@ export async function getPageviewStats(...args: [websiteId: string, filters: Que async function relationalQuery(websiteId: string, filters: QueryFilters) { const { timezone = 'utc', unit = 'day' } = filters; const { getDateSQL, parseFilters, rawQuery } = prisma; - const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({ + const { filterQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, websiteId, eventType: EVENT_TYPE.pageView, @@ -44,7 +44,7 @@ async function clickhouseQuery( ): Promise<{ x: string; y: number }[]> { const { timezone = 'utc', unit = 'day' } = filters; const { parseFilters, rawQuery, getDateSQL } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, eventType: EVENT_TYPE.pageView, diff --git a/src/queries/sql/reports/getAttribution.ts b/src/queries/sql/reports/getAttribution.ts index 75729695..df452343 100644 --- a/src/queries/sql/reports/getAttribution.ts +++ b/src/queries/sql/reports/getAttribution.ts @@ -2,8 +2,9 @@ import clickhouse from '@/lib/clickhouse'; import { EVENT_TYPE } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; -export interface AttributionCriteria { +export interface AttributionParameters { startDate: Date; endDate: Date; model: string; @@ -23,7 +24,9 @@ export interface AttributionResult { total: { pageviews: number; visitors: number; visits: number }; } -export async function getAttribution(...args: [websiteId: string, criteria: AttributionCriteria]) { +export async function getAttribution( + ...args: [websiteId: string, parameters: AttributionParameters, filters: QueryFilters] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -32,12 +35,18 @@ export async function getAttribution(...args: [websiteId: string, criteria: Attr async function relationalQuery( websiteId: string, - criteria: AttributionCriteria, + parameters: AttributionParameters, + filters: QueryFilters, ): Promise { - const { startDate, endDate, model, type, step, currency } = criteria; - const { rawQuery } = prisma; + const { model, type, currency } = parameters; + const { rawQuery, parseFilters } = prisma; const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent; const column = type === 'page' ? 'url_path' : 'event_name'; + const { filterQuery, queryParams } = parseFilters({ + ...filters, + ...parameters, + eventType, + }); function getUTMQuery(utmColumn: string) { return ` @@ -64,8 +73,9 @@ async function relationalQuery( from website_event where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} - and ${column} = {{conversionStep}} + and ${column} = {{step}} and event_type = {{eventType}} + ${filterQuery} group by 1),`; const revenueEventQuery = `WITH events AS ( @@ -76,8 +86,9 @@ async function relationalQuery( from revenue where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} - and ${column} = {{conversionStep}} + and ${column} = {{step}} and currency = {{currency}} + ${filterQuery} group by 1),`; function getModelQuery(model: string) { @@ -128,7 +139,7 @@ async function relationalQuery( order by 2 desc limit 20 `, - { websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const paidAdsres = await rawQuery( @@ -161,7 +172,7 @@ async function relationalQuery( FROM results ${currency ? '' : `WHERE name != ''`} `, - { websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const sourceRes = await rawQuery( @@ -170,7 +181,7 @@ async function relationalQuery( ${getModelQuery(model)} ${getUTMQuery('utm_source')} `, - { websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const mediumRes = await rawQuery( @@ -179,7 +190,7 @@ async function relationalQuery( ${getModelQuery(model)} ${getUTMQuery('utm_medium')} `, - { websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const campaignRes = await rawQuery( @@ -188,7 +199,7 @@ async function relationalQuery( ${getModelQuery(model)} ${getUTMQuery('utm_campaign')} `, - { websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const contentRes = await rawQuery( @@ -197,7 +208,7 @@ async function relationalQuery( ${getModelQuery(model)} ${getUTMQuery('utm_content')} `, - { websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const termRes = await rawQuery( @@ -206,7 +217,7 @@ async function relationalQuery( ${getModelQuery(model)} ${getUTMQuery('utm_term')} `, - { websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const totalRes = await rawQuery( @@ -218,10 +229,11 @@ async function relationalQuery( from website_event where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} - and ${column} = {{conversionStep}} + and ${column} = {{step}} and event_type = {{eventType}} + ${filterQuery} `, - { websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ).then(result => result?.[0]); return { @@ -238,13 +250,19 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - criteria: AttributionCriteria, + parameters: AttributionParameters, + filters: QueryFilters, ): Promise { - const { startDate, endDate, model, type, step, currency } = criteria; + const { model, type, currency } = parameters; const { rawQuery, parseFilters } = clickhouse; const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent; const column = type === 'page' ? 'url_path' : 'event_name'; - const { filterQuery, queryParams } = await parseFilters(criteria); + const { filterQuery, queryParams } = parseFilters({ + ...filters, + ...parameters, + websiteId, + eventType, + }); function getUTMQuery(utmColumn: string) { return ` @@ -301,7 +319,7 @@ async function clickhouseQuery( from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and ${column} = {conversionStep:String} + and ${column} = {step:String} and event_type = {eventType:UInt32} group by 1),`; @@ -313,7 +331,7 @@ async function clickhouseQuery( from website_revenue where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and ${column} = {conversionStep:String} + and ${column} = {step:String} and currency = {currency:String} group by 1),`; @@ -345,7 +363,7 @@ async function clickhouseQuery( order by 2 desc limit 20 `, - { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const paidAdsres = await rawQuery< @@ -376,7 +394,7 @@ async function clickhouseQuery( order by 2 desc limit 20 `, - { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const sourceRes = await rawQuery< @@ -390,7 +408,7 @@ async function clickhouseQuery( ${getModelQuery(model)} ${getUTMQuery('utm_source')} `, - { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const mediumRes = await rawQuery< @@ -404,7 +422,7 @@ async function clickhouseQuery( ${getModelQuery(model)} ${getUTMQuery('utm_medium')} `, - { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const campaignRes = await rawQuery< @@ -418,7 +436,7 @@ async function clickhouseQuery( ${getModelQuery(model)} ${getUTMQuery('utm_campaign')} `, - { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const contentRes = await rawQuery< @@ -432,7 +450,7 @@ async function clickhouseQuery( ${getModelQuery(model)} ${getUTMQuery('utm_content')} `, - { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const termRes = await rawQuery< @@ -446,7 +464,7 @@ async function clickhouseQuery( ${getModelQuery(model)} ${getUTMQuery('utm_term')} `, - { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ); const totalRes = await rawQuery<{ pageviews: number; visitors: number; visits: number }>( @@ -458,11 +476,11 @@ async function clickhouseQuery( from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and ${column} = {conversionStep:String} + and ${column} = {step:String} and event_type = {eventType:UInt32} ${filterQuery} `, - { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, + queryParams, ).then(result => result?.[0]); return { diff --git a/src/queries/sql/reports/getBreakdown.ts b/src/queries/sql/reports/getBreakdown.ts index 0730735f..7785da44 100644 --- a/src/queries/sql/reports/getBreakdown.ts +++ b/src/queries/sql/reports/getBreakdown.ts @@ -4,8 +4,19 @@ import clickhouse from '@/lib/clickhouse'; import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; import { QueryFilters } from '@/lib/types'; +export interface BreakdownParameters { + startDate: Date; + endDate: Date; + fields: string[]; +} + +export interface BreakdownData { + x: string; + y: number; +} + export async function getBreakdown( - ...args: [websiteId: string, fields: string[], filters: QueryFilters] + ...args: [websiteId: string, parameters: BreakdownParameters, filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -15,22 +26,21 @@ export async function getBreakdown( async function relationalQuery( websiteId: string, - fields: string[], + parameters: BreakdownParameters, filters: QueryFilters, -): Promise< - { - x: string; - y: number; - }[] -> { +): Promise { const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma; - const { filterQuery, joinSessionQuery, queryParams } = await parseFilters( + const { startDate, endDate, fields } = parameters; + const { filterQuery, joinSessionQuery, queryParams } = parseFilters( { ...filters, + websiteId, + startDate, + endDate, eventType: EVENT_TYPE.pageView, }, { - joinSession: !!fields.find(name => SESSION_COLUMNS.includes(name)), + joinSession: !!fields.find((name: string) => SESSION_COLUMNS.includes(name)), }, ); @@ -70,17 +80,16 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - fields: string[], + parameters: BreakdownParameters, filters: QueryFilters, -): Promise< - { - x: string; - y: number; - }[] -> { +): Promise { const { parseFilters, rawQuery } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ + const { startDate, endDate, fields } = parameters; + const { filterQuery, queryParams } = parseFilters({ ...filters, + websiteId, + startDate, + endDate, eventType: EVENT_TYPE.pageView, }); diff --git a/src/queries/sql/reports/getFunnel.ts b/src/queries/sql/reports/getFunnel.ts index 79ca5410..718ee0b3 100644 --- a/src/queries/sql/reports/getFunnel.ts +++ b/src/queries/sql/reports/getFunnel.ts @@ -1,15 +1,24 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; -export interface FunnelCriteria { - windowMinutes: number; +export interface FunnelParameters { startDate: Date; endDate: Date; + window: number; steps: { type: string; value: string }[]; } -export async function getFunnel(...args: [websiteId: string, criteria: FunnelCriteria]) { +export interface FunnelResult { + value: string; + visitors: number; + dropoff: number; +} + +export async function getFunnel( + ...args: [websiteId: string, parameters: FunnelParameters, filters: QueryFilters] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -18,21 +27,18 @@ export async function getFunnel(...args: [websiteId: string, criteria: FunnelCri async function relationalQuery( websiteId: string, - criteria: FunnelCriteria, -): Promise< - { - value: string; - visitors: number; - dropoff: number; - }[] -> { - const { windowMinutes, startDate, endDate, steps } = criteria; - const { rawQuery, getAddIntervalQuery } = prisma; - const { levelOneQuery, levelQuery, sumQuery, params } = getFunnelQuery(steps, windowMinutes); + parameters: FunnelParameters, + filters: QueryFilters, +): Promise { + const { startDate, endDate, window, steps } = parameters; + const { rawQuery, getAddIntervalQuery, parseFilters } = prisma; + const { levelOneQuery, levelQuery, sumQuery, params } = getFunnelQuery(steps, window); + + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate }); function getFunnelQuery( steps: { type: string; value: string }[], - windowMinutes: number, + window: number, ): { levelOneQuery: string; levelQuery: string; @@ -62,6 +68,7 @@ async function relationalQuery( where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} and ${column} ${operator} {{${i}}} + ${filterQuery} )`; } else { pv.levelQuery += ` @@ -73,7 +80,7 @@ async function relationalQuery( where we.website_id = {{websiteId::uuid}} and we.created_at between l.created_at and ${getAddIntervalQuery( `l.created_at `, - `${windowMinutes} minute`, + `${window} minute`, )} and we.${column} ${operator} {{${i}}} and we.created_at <= {{endDate}} @@ -102,17 +109,16 @@ async function relationalQuery( ORDER BY level; `, { - websiteId, - startDate, - endDate, ...params, + ...queryParams, }, ).then(formatResults(steps)); } async function clickhouseQuery( websiteId: string, - criteria: FunnelCriteria, + parameters: FunnelParameters, + filters: QueryFilters, ): Promise< { value: string; @@ -120,17 +126,17 @@ async function clickhouseQuery( dropoff: number; }[] > { - const { windowMinutes, startDate, endDate, steps } = criteria; + const { startDate, endDate, window, steps } = parameters; const { rawQuery, parseFilters } = clickhouse; const { levelOneQuery, levelQuery, sumQuery, stepFilterQuery, params } = getFunnelQuery( steps, - windowMinutes, + window, ); - const { filterQuery, queryParams } = await parseFilters(criteria); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate }); function getFunnelQuery( steps: { type: string; value: string }[], - windowMinutes: number, + window: number, ): { levelOneQuery: string; levelQuery: string; @@ -172,7 +178,7 @@ async function clickhouseQuery( from level${i} x join level0 y on x.session_id = y.session_id - where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute + where y.created_at between x.created_at and x.created_at + interval ${window} minute and y.${column} ${operator} {param${i}:String} )`; } @@ -211,9 +217,6 @@ async function clickhouseQuery( ) ORDER BY level; `, { - websiteId, - startDate, - endDate, ...params, ...queryParams, }, diff --git a/src/queries/sql/reports/getGoal.ts b/src/queries/sql/reports/getGoal.ts index ead23440..c659bb47 100644 --- a/src/queries/sql/reports/getGoal.ts +++ b/src/queries/sql/reports/getGoal.ts @@ -3,30 +3,37 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import { QueryFilters } from '@/lib/types'; -export interface GoalCriteria { +export interface GoalParameters { startDate: Date; endDate: Date; type: string; value: string; operator?: string; property?: string; - filters: QueryFilters; } -export async function getGoal(...args: [websiteId: string, criteria: GoalCriteria]) { +export async function getGoal( + ...args: [websiteId: string, params: GoalParameters, filters: QueryFilters] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId: string, criteria: GoalCriteria) { - const { type, value, filters } = criteria; +async function relationalQuery( + websiteId: string, + parameters: GoalParameters, + filters: QueryFilters, +) { + const { startDate, endDate, type, value } = parameters; const { rawQuery, parseFilters } = prisma; - const { filterQuery, dateQuery, queryParams } = await parseFilters({ + const { filterQuery, dateQuery, queryParams } = parseFilters({ ...filters, websiteId, value, + startDate, + endDate, }); const isPage = type === 'page'; const column = isPage ? 'url_path' : 'event_name'; @@ -53,13 +60,19 @@ async function relationalQuery(websiteId: string, criteria: GoalCriteria) { ); } -async function clickhouseQuery(websiteId: string, criteria: GoalCriteria) { - const { type, value, filters } = criteria; +async function clickhouseQuery( + websiteId: string, + parameters: GoalParameters, + filters: QueryFilters, +) { + const { startDate, endDate, type, value } = parameters; const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, dateQuery, queryParams } = await parseFilters({ + const { filterQuery, dateQuery, queryParams } = parseFilters({ ...filters, websiteId, value, + startDate, + endDate, }); const isPage = type === 'page'; const column = isPage ? 'url_path' : 'event_name'; diff --git a/src/queries/sql/reports/getJourney.ts b/src/queries/sql/reports/getJourney.ts index 897d1080..4145c14d 100644 --- a/src/queries/sql/reports/getJourney.ts +++ b/src/queries/sql/reports/getJourney.ts @@ -1,8 +1,17 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; -interface JourneyResult { +export interface JourneyParameters { + startDate: Date; + endDate: Date; + steps: number; + startStep?: string; + endStep?: string; +} + +export interface JourneyResult { e1: string; e2: string; e3: string; @@ -14,16 +23,7 @@ interface JourneyResult { } export async function getJourney( - ...args: [ - websiteId: string, - filters: { - startDate: Date; - endDate: Date; - steps: number; - startStep?: string; - endStep?: string; - }, - ] + ...args: [websiteId: string, parameters: JourneyParameters, filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -33,21 +33,17 @@ export async function getJourney( async function relationalQuery( websiteId: string, - filters: { - startDate: Date; - endDate: Date; - steps: number; - startStep?: string; - endStep?: string; - }, + parameters: JourneyParameters, + filters: QueryFilters, ): Promise { - const { startDate, endDate, steps, startStep, endStep } = filters; - const { rawQuery } = prisma; + const { startDate, endDate, steps, startStep, endStep } = parameters; + const { rawQuery, parseFilters } = prisma; const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery( steps, startStep, endStep, ); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate }); function getJourneyQuery( steps: number, @@ -123,6 +119,7 @@ async function relationalQuery( from website_event where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}}), + ${filterQuery} ${sequenceQuery} select * from sequences @@ -133,31 +130,25 @@ async function relationalQuery( limit 100 `, { - websiteId, - startDate, - endDate, ...params, + ...queryParams, }, ).then(parseResult); } async function clickhouseQuery( websiteId: string, - filters: { - startDate: Date; - endDate: Date; - steps: number; - startStep?: string; - endStep?: string; - }, + parameters: JourneyParameters, + filters: QueryFilters, ): Promise { - const { startDate, endDate, steps, startStep, endStep } = filters; + const { startDate, endDate, steps, startStep, endStep } = parameters; const { rawQuery, parseFilters } = clickhouse; const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery( steps, startStep, endStep, ); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate }); function getJourneyQuery( steps: number, @@ -222,8 +213,6 @@ async function clickhouseQuery( }; } - const { filterQuery, queryParams } = await parseFilters(filters); - return rawQuery( ` WITH events AS ( @@ -245,9 +234,6 @@ async function clickhouseQuery( limit 100 `, { - websiteId, - startDate, - endDate, ...params, ...queryParams, }, diff --git a/src/queries/sql/reports/getRetention.ts b/src/queries/sql/reports/getRetention.ts index 30d9b6d9..c552ae45 100644 --- a/src/queries/sql/reports/getRetention.ts +++ b/src/queries/sql/reports/getRetention.ts @@ -1,8 +1,9 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; -export interface RetentionCriteria { +export interface RetentionParameters { startDate: Date; endDate: Date; timezone?: string; @@ -16,7 +17,9 @@ export interface RetentionResult { percentage: number; } -export async function getRetention(...args: [websiteId: string, criteria: RetentionCriteria]) { +export async function getRetention( + ...args: [websiteId: string, parameters: RetentionParameters, filters: QueryFilters] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -25,13 +28,20 @@ export async function getRetention(...args: [websiteId: string, criteria: Retent async function relationalQuery( websiteId: string, - criteria: RetentionCriteria, + parameters: RetentionParameters, + filters: QueryFilters, ): Promise { - const { startDate, endDate, timezone } = criteria; + const { startDate, endDate, timezone } = parameters; const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery, parseFilters } = prisma; const unit = 'day'; - const { filterQuery, queryParams } = await parseFilters(criteria); + const { filterQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + startDate, + endDate, + timezone, + }); return rawQuery( ` @@ -81,24 +91,26 @@ async function relationalQuery( on c.cohort_date = s.cohort_date where c.day_number <= 31 order by 1, 2`, - { - websiteId, - startDate, - endDate, - ...queryParams, - }, + queryParams, ); } async function clickhouseQuery( websiteId: string, - criteria: RetentionCriteria, + parameters: RetentionParameters, + filters: QueryFilters, ): Promise { - const { startDate, endDate, timezone } = criteria; + const { startDate, endDate, timezone } = parameters; const { getDateSQL, rawQuery, parseFilters } = clickhouse; const unit = 'day'; - const { filterQuery, queryParams } = await parseFilters(criteria); + const { filterQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + startDate, + endDate, + timezone, + }); return rawQuery( ` @@ -150,11 +162,6 @@ async function clickhouseQuery( on c.cohort_date = s.cohort_date where c.day_number <= 31 order by 1, 2`, - { - websiteId, - startDate, - endDate, - ...queryParams, - }, + queryParams, ); } diff --git a/src/queries/sql/reports/getRevenue.ts b/src/queries/sql/reports/getRevenue.ts index 94988df0..20d77297 100644 --- a/src/queries/sql/reports/getRevenue.ts +++ b/src/queries/sql/reports/getRevenue.ts @@ -1,8 +1,9 @@ import clickhouse from '@/lib/clickhouse'; -import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; -export interface RevenueCriteria { +export interface RevenuParameters { startDate: Date; endDate: Date; unit: string; @@ -21,7 +22,9 @@ export interface RevenueResult { }[]; } -export async function getRevenue(...args: [websiteId: string, criteria: RevenueCriteria]) { +export async function getRevenue( + ...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -30,12 +33,12 @@ export async function getRevenue(...args: [websiteId: string, criteria: RevenueC async function relationalQuery( websiteId: string, - criteria: RevenueCriteria, + parameters: RevenuParameters, + filters: QueryFilters, ): Promise { - const { startDate, endDate, unit = 'day', currency } = criteria; - const { getDateSQL, rawQuery } = prisma; - const db = getDatabaseType(); - const like = db === POSTGRESQL ? 'ilike' : 'like'; + const { startDate, endDate, currency, unit = 'day' } = parameters; + const { getDateSQL, rawQuery, parseFilters } = prisma; + const { queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate, currency }); const chart = await rawQuery( ` @@ -50,7 +53,7 @@ async function relationalQuery( group by x, t order by t `, - { websiteId, startDate, endDate, unit, currency }, + queryParams, ); const country = await rawQuery( @@ -63,10 +66,10 @@ async function relationalQuery( on s.session_id = r.session_id where r.website_id = {{websiteId::uuid}} and r.created_at between {{startDate}} and {{endDate}} - and r.currency ${like} {{currency}} + and r.currency = {{currency}} group by s.country `, - { websiteId, startDate, endDate, currency }, + queryParams, ); const total = await rawQuery( @@ -78,9 +81,9 @@ async function relationalQuery( from revenue r where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} - and currency ${like} {{currency}} + and currency = {{currency}} `, - { websiteId, startDate, endDate, currency }, + queryParams, ).then(result => result?.[0]); total.average = total.count > 0 ? total.sum / total.count : 0; @@ -98,7 +101,7 @@ async function relationalQuery( group by currency order by sum desc `, - { websiteId, startDate, endDate, unit, currency }, + queryParams, ); return { chart, country, table, total }; @@ -106,10 +109,18 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - criteria: RevenueCriteria, + parameters: RevenuParameters, + filters: QueryFilters, ): Promise { - const { startDate, endDate, unit = 'day', currency } = criteria; - const { getDateSQL, rawQuery } = clickhouse; + const { startDate, endDate, unit = 'day', currency } = parameters; + const { getDateSQL, rawQuery, parseFilters } = clickhouse; + const { queryParams } = parseFilters({ + ...filters, + websiteId, + startDate, + endDate, + currency, + }); const chart = await rawQuery< { @@ -130,7 +141,7 @@ async function clickhouseQuery( group by x, t order by t `, - { websiteId, startDate, endDate, unit, currency }, + queryParams, ); const country = await rawQuery< @@ -144,9 +155,11 @@ async function clickhouseQuery( s.country as name, sum(w.revenue) as value from website_revenue w - join (select distinct website_id, session_id, country - from website_event - where website_id = {websiteId:UUID}) s + join ( + select distinct website_id, session_id, country + from website_event + where website_id = {websiteId:UUID} + ) s on w.website_id = s.website_id and w.session_id = s.session_id where w.website_id = {websiteId:UUID} @@ -154,7 +167,7 @@ async function clickhouseQuery( and w.currency = {currency:String} group by s.country `, - { websiteId, startDate, endDate, currency }, + queryParams, ); const total = await rawQuery<{ @@ -172,7 +185,7 @@ async function clickhouseQuery( and created_at between {startDate:DateTime64} and {endDate:DateTime64} and currency = {currency:String} `, - { websiteId, startDate, endDate, currency }, + queryParams, ).then(result => result?.[0]); total.average = total.count > 0 ? total.sum / total.count : 0; @@ -197,7 +210,7 @@ async function clickhouseQuery( group by currency order by sum desc `, - { websiteId, startDate, endDate, unit, currency }, + queryParams, ); return { chart, country, table, total }; diff --git a/src/queries/sql/reports/getUTM.ts b/src/queries/sql/reports/getUTM.ts index 84534493..9fda7d83 100644 --- a/src/queries/sql/reports/getUTM.ts +++ b/src/queries/sql/reports/getUTM.ts @@ -1,23 +1,30 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; -export interface UTMCriteria { +export interface UTMParameters { startDate: Date; endDate: Date; } -export async function getUTM(...args: [websiteId: string, criteria: UTMCriteria]) { +export async function getUTM( + ...args: [websiteId: string, parameters: UTMParameters, filters: QueryFilters] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId: string, criteria: UTMCriteria) { - const { startDate, endDate } = criteria; +async function relationalQuery( + websiteId: string, + parameters: UTMParameters, + filters: QueryFilters, +) { + const { startDate, endDate } = parameters; const { rawQuery, parseFilters } = prisma; - const { filterQuery, queryParams } = await parseFilters(criteria); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate }); return rawQuery( ` @@ -30,19 +37,18 @@ async function relationalQuery(websiteId: string, criteria: UTMCriteria) { ${filterQuery} group by 1 `, - { - ...queryParams, - websiteId, - startDate, - endDate, - }, + queryParams, ).then(result => parseParameters(result as any[])); } -async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) { - const { startDate, endDate } = criteria; +async function clickhouseQuery( + websiteId: string, + parameters: UTMParameters, + filters: QueryFilters, +) { + const { startDate, endDate } = parameters; const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters(criteria); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate }); return rawQuery( ` @@ -55,12 +61,7 @@ async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) { ${filterQuery} group by 1 `, - { - ...queryParams, - websiteId, - startDate, - endDate, - }, + queryParams, ).then(result => parseParameters(result as any[])); } diff --git a/src/queries/sql/sessions/getSessionDataProperties.ts b/src/queries/sql/sessions/getSessionDataProperties.ts index ec96850f..13a68fa4 100644 --- a/src/queries/sql/sessions/getSessionDataProperties.ts +++ b/src/queries/sql/sessions/getSessionDataProperties.ts @@ -17,7 +17,7 @@ async function relationalQuery( filters: QueryFilters & { propertyName?: string }, ) { const { rawQuery, parseFilters } = prisma; - const { filterQuery, queryParams } = await parseFilters(filters, { + const { filterQuery, queryParams } = parseFilters(filters, { columns: { propertyName: 'data_key' }, }); @@ -45,7 +45,7 @@ async function clickhouseQuery( filters: QueryFilters & { propertyName?: string }, ): Promise<{ propertyName: string; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters(filters, { + const { filterQuery, queryParams } = parseFilters(filters, { columns: { propertyName: 'data_key' }, }); diff --git a/src/queries/sql/sessions/getSessionDataValues.ts b/src/queries/sql/sessions/getSessionDataValues.ts index 4ca44029..6d530b69 100644 --- a/src/queries/sql/sessions/getSessionDataValues.ts +++ b/src/queries/sql/sessions/getSessionDataValues.ts @@ -17,7 +17,7 @@ async function relationalQuery( filters: QueryFilters & { propertyName?: string }, ) { const { rawQuery, parseFilters, getDateSQL } = prisma; - const { filterQuery, queryParams } = await parseFilters(filters); + const { filterQuery, queryParams } = parseFilters(filters); return rawQuery( ` @@ -48,7 +48,7 @@ async function clickhouseQuery( filters: QueryFilters & { propertyName?: string }, ): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> { const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters(filters); + const { filterQuery, queryParams } = parseFilters(filters); return rawQuery( ` diff --git a/src/queries/sql/sessions/getSessionMetrics.ts b/src/queries/sql/sessions/getSessionMetrics.ts index f62cd115..ca0a8c2f 100644 --- a/src/queries/sql/sessions/getSessionMetrics.ts +++ b/src/queries/sql/sessions/getSessionMetrics.ts @@ -4,14 +4,14 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; import { QueryFilters } from '@/lib/types'; +export interface SessionMetricsParameters { + type: string; + limit: number | string; + offset: number | string; +} + export async function getSessionMetrics( - ...args: [ - websiteId: string, - type: string, - filters: QueryFilters, - limit?: number | string, - offset?: number | string, - ] + ...args: [websiteId: string, parameters: SessionMetricsParameters, filters: QueryFilters] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -21,16 +21,16 @@ export async function getSessionMetrics( async function relationalQuery( websiteId: string, - type: string, + parameters: SessionMetricsParameters, filters: QueryFilters, - limit: number | string = 500, - offset: number | string = 0, ) { + const { type, limit = 500, offset = 0 } = parameters; const column = FILTER_COLUMNS[type] || type; const { parseFilters, rawQuery } = prisma; - const { filterQuery, joinSessionQuery, queryParams } = await parseFilters( + const { filterQuery, joinSessionQuery, queryParams } = parseFilters( { ...filters, + websiteId, eventType: EVENT_TYPE.pageView, }, { @@ -57,21 +57,21 @@ async function relationalQuery( limit ${limit} offset ${offset} `, - queryParams, + { ...queryParams, ...parameters }, ); } async function clickhouseQuery( websiteId: string, - type: string, + parameters: SessionMetricsParameters, filters: QueryFilters, - limit: number | string = 500, - offset: number | string = 0, ): Promise<{ x: string; y: number }[]> { + const { type, limit = 500, offset = 0 } = parameters; const column = FILTER_COLUMNS[type] || type; const { parseFilters, rawQuery } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ + const { filterQuery, queryParams } = parseFilters({ ...filters, + websiteId, eventType: EVENT_TYPE.pageView, }); const includeCountry = column === 'city' || column === 'region'; @@ -114,5 +114,5 @@ async function clickhouseQuery( `; } - return rawQuery(sql, queryParams); + return rawQuery(sql, { ...queryParams, ...parameters }); } diff --git a/src/queries/sql/sessions/getSessionStats.ts b/src/queries/sql/sessions/getSessionStats.ts index 067f5640..bb3ff03d 100644 --- a/src/queries/sql/sessions/getSessionStats.ts +++ b/src/queries/sql/sessions/getSessionStats.ts @@ -14,8 +14,9 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer async function relationalQuery(websiteId: string, filters: QueryFilters) { const { timezone = 'utc', unit = 'day' } = filters; const { getDateSQL, parseFilters, rawQuery } = prisma; - const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({ + const { filterQuery, joinSessionQuery, queryParams } = parseFilters({ ...filters, + websiteId, eventType: EVENT_TYPE.pageView, }); @@ -43,8 +44,9 @@ async function clickhouseQuery( ): Promise<{ x: string; y: number }[]> { const { timezone = 'utc', unit = 'day' } = filters; const { parseFilters, rawQuery, getDateSQL } = clickhouse; - const { filterQuery, queryParams } = await parseFilters({ + const { filterQuery, queryParams } = parseFilters({ ...filters, + websiteId, eventType: EVENT_TYPE.pageView, }); diff --git a/src/queries/sql/sessions/getWebsiteSessionStats.ts b/src/queries/sql/sessions/getWebsiteSessionStats.ts index b972a4e9..5e4d2721 100644 --- a/src/queries/sql/sessions/getWebsiteSessionStats.ts +++ b/src/queries/sql/sessions/getWebsiteSessionStats.ts @@ -25,7 +25,7 @@ async function relationalQuery( filters: QueryFilters, ): Promise { const { parseFilters, rawQuery } = prisma; - const { filterQuery, queryParams } = await parseFilters(filters); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId }); return rawQuery( ` @@ -50,7 +50,7 @@ async function clickhouseQuery( filters: QueryFilters, ): Promise { const { rawQuery, parseFilters } = clickhouse; - const { filterQuery, queryParams } = await parseFilters(filters); + const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId }); return rawQuery( ` diff --git a/src/queries/sql/sessions/getWebsiteSessions.ts b/src/queries/sql/sessions/getWebsiteSessions.ts index 1f8d56f2..be118cc6 100644 --- a/src/queries/sql/sessions/getWebsiteSessions.ts +++ b/src/queries/sql/sessions/getWebsiteSessions.ts @@ -12,7 +12,14 @@ export async function getWebsiteSessions(...args: [websiteId: string, filters: Q async function relationalQuery(websiteId: string, filters: QueryFilters) { const { pagedRawQuery, parseFilters } = prisma; - const { filterQuery, dateQuery, queryParams } = await parseFilters(filters); + const { search } = filters; + const { filterQuery, dateQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + search: search ? `%${search}%` : undefined, + }); + + const searchQuery = search ? `and session.distinct_id ilike {{search}}` : ''; return pagedRawQuery( ` @@ -38,6 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { where website_event.website_id = {{websiteId::uuid}} ${dateQuery} ${filterQuery} + ${searchQuery} group by session.session_id, session.website_id, website_event.hostname, @@ -58,7 +66,13 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) { const { pagedRawQuery, parseFilters, getDateStringSQL } = clickhouse; - const { filterQuery, dateQuery, queryParams } = await parseFilters(filters); + const { search } = filters; + const { filterQuery, dateQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + }); + + const searchQuery = search ? `and positionCaseInsensitive(distinct_id, {search:String}) > 0` : ''; return pagedRawQuery( ` @@ -83,6 +97,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { where website_id = {websiteId:UUID} ${dateQuery} ${filterQuery} + ${searchQuery} group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city order by lastAt desc `, diff --git a/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts b/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts index 88bb9806..1be8d122 100644 --- a/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts +++ b/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts @@ -15,7 +15,7 @@ export async function getWebsiteSessionsWeekly( async function relationalQuery(websiteId: string, filters: QueryFilters) { const { timezone = 'utc' } = filters; const { rawQuery, getDateWeeklySQL, parseFilters } = prisma; - const { queryParams } = await parseFilters(filters); + const { queryParams } = parseFilters(filters); return rawQuery( ` @@ -35,7 +35,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) { const { timezone = 'utc' } = filters; const { rawQuery, parseFilters } = clickhouse; - const { queryParams } = await parseFilters(filters); + const { queryParams } = parseFilters(filters); return rawQuery( `