diff --git a/src/app/(main)/reports/[reportId]/Report.module.css b/src/app/(main)/reports/[reportId]/Report.module.css index db65d001..6aa6a9b3 100644 --- a/src/app/(main)/reports/[reportId]/Report.module.css +++ b/src/app/(main)/reports/[reportId]/Report.module.css @@ -3,5 +3,5 @@ grid-template-rows: max-content 1fr; grid-template-columns: max-content 1fr; margin-bottom: 60px; - height: 100%; + height: 90vh; } diff --git a/src/app/(main)/reports/create/ReportTemplates.tsx b/src/app/(main)/reports/create/ReportTemplates.tsx index 0777cc1f..e37efc03 100644 --- a/src/app/(main)/reports/create/ReportTemplates.tsx +++ b/src/app/(main)/reports/create/ReportTemplates.tsx @@ -1,14 +1,14 @@ -import Link from 'next/link'; -import { Button, Icons, Text, Icon } from 'react-basics'; -import PageHeader from 'components/layout/PageHeader'; import Funnel from 'assets/funnel.svg'; import Lightbulb from 'assets/lightbulb.svg'; import Magnet from 'assets/magnet.svg'; +import Path from 'assets/path.svg'; import Tag from 'assets/tag.svg'; import Target from 'assets/target.svg'; -import Path from 'assets/path.svg'; -import styles from './ReportTemplates.module.css'; import { useMessages, useTeamUrl } from 'components/hooks'; +import PageHeader from 'components/layout/PageHeader'; +import Link from 'next/link'; +import { Button, Icon, Icons, Text } from 'react-basics'; +import styles from './ReportTemplates.module.css'; export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/reports/insights/InsightsTable.tsx b/src/app/(main)/reports/insights/InsightsTable.tsx index 2cccc24c..692d7824 100644 --- a/src/app/(main)/reports/insights/InsightsTable.tsx +++ b/src/app/(main)/reports/insights/InsightsTable.tsx @@ -3,6 +3,7 @@ import { GridTable, GridColumn } from 'react-basics'; import { useFormat, useMessages } from 'components/hooks'; import { ReportContext } from '../[reportId]/Report'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import { formatShortTime } from 'lib/format'; export function InsightsTable() { const [fields, setFields] = useState([]); @@ -31,6 +32,12 @@ export function InsightsTable() { ); })} + + {row => row?.views?.toLocaleString()} + + + {row => row?.visits?.toLocaleString()} + {row => row?.visitors?.toLocaleString()} - - {row => row?.views?.toLocaleString()} + + {row => { + const n = (Math.min(row?.visits, row?.bounces) / row?.visits) * 100; + return Math.round(+n) + '%'; + }} + + + {row => { + const n = row?.totaltime / row?.visits; + return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`; + }} ); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index 246a54b8..a6e7ad40 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -57,11 +57,11 @@ export function WebsiteMetricsBar({ }, { label: formatMessage(labels.bounceRate), - value: (Math.min(visitors.value, bounces.value) / visitors.value) * 100, - prev: (Math.min(visitors.prev, bounces.prev) / visitors.prev) * 100, + value: (Math.min(visits.value, bounces.value) / visits.value) * 100, + prev: (Math.min(visits.prev, bounces.prev) / visits.prev) * 100, change: - (Math.min(visitors.value, bounces.value) / visitors.value) * 100 - - (Math.min(visitors.prev, bounces.prev) / visitors.prev) * 100, + (Math.min(visits.value, bounces.value) / visits.value) * 100 - + (Math.min(visits.prev, bounces.prev) / visits.prev) * 100, formatValue: n => Math.round(+n) + '%', reverseColors: true, }, diff --git a/src/components/layout/Page.module.css b/src/components/layout/Page.module.css index d1498122..52893157 100644 --- a/src/components/layout/Page.module.css +++ b/src/components/layout/Page.module.css @@ -4,7 +4,6 @@ flex-direction: column; position: relative; width: 100%; - height: 100%; max-width: 1320px; margin: 0 auto; padding: 0 20px; diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index e5cb40c0..c1b9d25f 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -119,7 +119,7 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio }; } -async function rawQuery(query: string, params: Record = {}): Promise { +async function rawQuery(query: string, params: Record = {}): Promise { if (process.env.LOG_QUERY) { log('QUERY:\n', query); log('PARAMETERS:\n', params); diff --git a/src/queries/analytics/reports/getInsights.ts b/src/queries/analytics/reports/getInsights.ts index 282ed755..7313e436 100644 --- a/src/queries/analytics/reports/getInsights.ts +++ b/src/queries/analytics/reports/getInsights.ts @@ -23,7 +23,7 @@ async function relationalQuery( y: number; }[] > { - const { parseFilters, rawQuery } = prisma; + const { getTimestampDiffQuery, parseFilters, rawQuery } = prisma; const { filterQuery, joinSession, params } = await parseFilters( websiteId, { @@ -37,15 +37,31 @@ async function relationalQuery( return rawQuery( ` - select + select + sum(t.c) as "views", + count(distinct t.session_id) as "visitors", + count(distinct t.visit_id) as "visits", + sum(case when t.c = 1 then 1 else 0 end) as "bounces", + sum(${getTimestampDiffQuery('t.min_time', 't.max_time')}) as "totaltime", ${parseFields(fields)} - from website_event - ${joinSession} - where website_event.website_id = {{websiteId::uuid}} - and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type = {{eventType}} - ${filterQuery} - ${parseGroupBy(fields)} + from ( + select + ${parseFields(fields)}, + website_event.session_id, + website_event.visit_id, + count(*) as "c", + min(website_event.created_at) as "min_time", + max(website_event.created_at) as "max_time" + from website_event + ${joinSession} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and event_type = {{eventType}} + ${filterQuery} + group by ${parseFields(fields)}, + website_event.session_id, website_event.visit_id + ) as t + group by ${parseFields(fields)} order by 1 desc, 2 desc limit 500 `, @@ -71,14 +87,30 @@ async function clickhouseQuery( return rawQuery( ` - select + select + sum(t.c) as "views", + count(distinct t.session_id) as "visitors", + count(distinct t.visit_id) as "visits", + sum(if(t.c = 1, 1, 0)) as "bounces", + sum(max_time-min_time) as "totaltime", ${parseFields(fields)} - from website_event - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type = {eventType:UInt32} - ${filterQuery} - ${parseGroupBy(fields)} + from ( + select + ${parseFields(fields)}, + session_id, + visit_id, + count(*) c, + min(created_at) min_time, + max(created_at) max_time + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = {eventType:UInt32} + ${filterQuery} + group by ${parseFields(fields)}, + session_id, visit_id + ) as t + group by ${parseFields(fields)} order by 1 desc, 2 desc limit 500 `, @@ -89,27 +121,14 @@ async function clickhouseQuery( ...a, views: Number(a.views), visitors: Number(a.visitors), + visits: Number(a.visits), + bounces: Number(a.bounces), + totaltime: Number(a.totaltime), }; }); }); } -function parseFields(fields: any[]) { - const query = fields.reduce( - (arr, field) => { - const { name } = field; - - return arr.concat(`${FILTER_COLUMNS[name]} as "${name}"`); - }, - ['count(*) as views', 'count(distinct website_event.session_id) as visitors'], - ); - - return query.join(',\n'); -} - -function parseGroupBy(fields: { name: any }[]) { - if (!fields.length) { - return ''; - } - return `group by ${fields.map(({ name }) => FILTER_COLUMNS[name]).join(',')}`; +function parseFields(fields: { name: any }[]) { + return `${fields.map(({ name }) => FILTER_COLUMNS[name]).join(',')}`; }