diff --git a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
index 0a054e8d..d039b67f 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
@@ -1,14 +1,14 @@
-import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
-import { Flexbox } from 'react-basics';
-import MetricsBar from 'components/metrics/MetricsBar';
-import MetricCard from 'components/metrics/MetricCard';
import { useMessages } from 'components/hooks';
-import useWebsiteStats from 'components/hooks/queries/useWebsiteStats';
+import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats';
+import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
+import MetricCard from 'components/metrics/MetricCard';
+import MetricsBar from 'components/metrics/MetricsBar';
import { formatLongNumber } from 'lib/format';
+import { Flexbox } from 'react-basics';
export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
- const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId);
+ const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);
return (
@@ -28,6 +28,11 @@ export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
label={formatMessage(labels.views)}
formatValue={formatLongNumber}
/>
+
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
index 9133ca71..803e7a06 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
@@ -1,14 +1,14 @@
-import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
-import { Flexbox } from 'react-basics';
-import MetricsBar from 'components/metrics/MetricsBar';
-import MetricCard from 'components/metrics/MetricCard';
import { useMessages } from 'components/hooks';
-import useWebsiteStats from 'components/hooks/queries/useWebsiteStats';
+import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats';
+import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
+import MetricCard from 'components/metrics/MetricCard';
+import MetricsBar from 'components/metrics/MetricsBar';
import { formatLongNumber } from 'lib/format';
+import { Flexbox } from 'react-basics';
export function SessionsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
- const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId);
+ const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);
return (
diff --git a/src/components/hooks/queries/useWebsiteSessionStats.ts b/src/components/hooks/queries/useWebsiteSessionStats.ts
new file mode 100644
index 00000000..7671b2eb
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSessionStats.ts
@@ -0,0 +1,16 @@
+import { useApi } from './useApi';
+import { useFilterParams } from '../useFilterParams';
+
+export function useWebsiteSessionStats(websiteId: string, options?: { [key: string]: string }) {
+ const { get, useQuery } = useApi();
+ const params = useFilterParams(websiteId);
+
+ return useQuery({
+ queryKey: ['sessions:stats', { websiteId, ...params }],
+ queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...params }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
+
+export default useWebsiteSessionStats;
diff --git a/src/pages/api/websites/[websiteId]/sessions/stats.ts b/src/pages/api/websites/[websiteId]/sessions/stats.ts
new file mode 100644
index 00000000..a522bd6b
--- /dev/null
+++ b/src/pages/api/websites/[websiteId]/sessions/stats.ts
@@ -0,0 +1,84 @@
+import { canViewWebsite } from 'lib/auth';
+import { useAuth, useCors, useValidate } from 'lib/middleware';
+import { getRequestDateRange, getRequestFilters } from 'lib/request';
+import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types';
+import { NextApiResponse } from 'next';
+import { methodNotAllowed, ok, unauthorized } from 'next-basics';
+import { getWebsiteSessionStats } from 'queries/analytics/sessions/getWebsiteSessionStats';
+import * as yup from 'yup';
+
+export interface WebsiteSessionStatsRequestQuery {
+ websiteId: string;
+ startAt: number;
+ endAt: number;
+ url?: string;
+ referrer?: string;
+ title?: string;
+ query?: string;
+ event?: string;
+ host?: string;
+ os?: string;
+ browser?: string;
+ device?: string;
+ country?: string;
+ region?: string;
+ city?: string;
+}
+
+const schema = {
+ GET: yup.object().shape({
+ websiteId: yup.string().uuid().required(),
+ startAt: yup.number().required(),
+ endAt: yup.number().required(),
+ url: yup.string(),
+ referrer: yup.string(),
+ title: yup.string(),
+ query: yup.string(),
+ event: yup.string(),
+ host: yup.string(),
+ os: yup.string(),
+ browser: yup.string(),
+ device: yup.string(),
+ country: yup.string(),
+ region: yup.string(),
+ city: yup.string(),
+ }),
+};
+
+export default async (
+ req: NextApiRequestQueryBody,
+ res: NextApiResponse,
+) => {
+ await useCors(req, res);
+ await useAuth(req, res);
+ await useValidate(schema, req, res);
+
+ const { websiteId } = req.query;
+
+ if (req.method === 'GET') {
+ if (!(await canViewWebsite(req.auth, websiteId))) {
+ return unauthorized(res);
+ }
+
+ const { startDate, endDate } = await getRequestDateRange(req);
+
+ const filters = getRequestFilters(req);
+
+ const metrics = await getWebsiteSessionStats(websiteId, {
+ ...filters,
+ startDate,
+ endDate,
+ });
+
+ const stats = Object.keys(metrics[0]).reduce((obj, key) => {
+ obj[key] = {
+ value: Number(metrics[0][key]) || 0,
+ };
+ return obj;
+ }, {});
+
+ return ok(res, stats);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/src/queries/analytics/events/getEventDataProperties.ts b/src/queries/analytics/events/getEventDataProperties.ts
index 8d1eaf9d..9d55f896 100644
--- a/src/queries/analytics/events/getEventDataProperties.ts
+++ b/src/queries/analytics/events/getEventDataProperties.ts
@@ -24,14 +24,15 @@ async function relationalQuery(
return rawQuery(
`
select
- event_name as "eventName",
- data_key as "propertyName",
+ we.event_name as "eventName",
+ ed.data_key as "propertyName",
count(*) as "total"
- from event_data
- where website_id = {{websiteId::uuid}}
- and created_at between {{startDate}} and {{endDate}}
+ from event_data ed
+ join website_event we on we.event_id = ed.website_event_id
+ where ed.website_id = {{websiteId::uuid}}
+ and ed.created_at between {{startDate}} and {{endDate}}
${filterQuery}
- group by event_name, data_key
+ group by we.event_name, ed.data_key
order by 3 desc
limit 500
`,
diff --git a/src/queries/analytics/getWebsiteStats.ts b/src/queries/analytics/getWebsiteStats.ts
index 17bf0c8d..c5141d3b 100644
--- a/src/queries/analytics/getWebsiteStats.ts
+++ b/src/queries/analytics/getWebsiteStats.ts
@@ -1,4 +1,5 @@
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';
@@ -22,13 +23,10 @@ async function relationalQuery(
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
- const { filterQuery, joinSession, params } = await parseFilters(
- websiteId,
- {
- ...filters,
- },
- { joinSession: true },
- );
+ const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
+ ...filters,
+ eventType: EVENT_TYPE.pageView,
+ });
return rawQuery(
`
@@ -36,14 +34,12 @@ async function relationalQuery(
sum(t.c) as "pageviews",
count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits",
- count(distinct t.country) as "countries",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
website_event.session_id,
website_event.visit_id,
- session.country,
count(*) as "c",
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
@@ -51,8 +47,9 @@ async function relationalQuery(
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
+ and event_type = {{eventType}}
${filterQuery}
- group by 1, 2, 3
+ group by 1, 2
) as t
`,
params,
@@ -68,6 +65,7 @@ async function clickhouseQuery(
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
+ eventType: EVENT_TYPE.pageView,
});
let sql = '';
@@ -78,22 +76,21 @@ async function clickhouseQuery(
sum(t.c) as "pageviews",
uniq(t.session_id) as "visitors",
uniq(t.visit_id) as "visits",
- uniq(t.country) as "countries",
- sumIf(1, t.c = 1) as "bounces",
+ sum(if(t.c = 1, 1, 0)) as "bounces",
sum(max_time-min_time) as "totaltime"
from (
select
session_id,
visit_id,
- country,
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 session_id, visit_id, country
+ group by session_id, visit_id
) as t;
`;
} else {
@@ -102,22 +99,20 @@ async function clickhouseQuery(
sum(t.c) as "pageviews",
uniq(session_id) as "visitors",
uniq(visit_id) as "visits",
- uniq(country) as "countries",
sumIf(1, t.c = 1) as "bounces",
sum(max_time-min_time) as "totaltime"
- from (
- select
- session_id,
- visit_id,
- country,
- sum(views) c,
- min(min_time) min_time,
- max(max_time) max_time
- from umami.website_event_stats_hourly "website_event"
- where website_id = {websiteId:UUID}
- and created_at between {startDate:DateTime64} and {endDate:DateTime64}
- ${filterQuery}
- group by session_id, visit_id, country
+ from (select
+ session_id,
+ visit_id,
+ sum(views) c,
+ min(min_time) min_time,
+ max(max_time) max_time
+ from umami.website_event_stats_hourly "website_event"
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ and event_type = {eventType:UInt32}
+ ${filterQuery}
+ group by session_id, visit_id
) as t;
`;
}
@@ -130,8 +125,6 @@ async function clickhouseQuery(
visits: Number(a.visits),
bounces: Number(a.bounces),
totaltime: Number(a.totaltime),
- countries: Number(a.countries),
- events: Number(a.events),
};
});
});
diff --git a/src/queries/analytics/sessions/getSessionData.ts b/src/queries/analytics/sessions/getSessionData.ts
index 8b78fc86..509fcdbf 100644
--- a/src/queries/analytics/sessions/getSessionData.ts
+++ b/src/queries/analytics/sessions/getSessionData.ts
@@ -32,7 +32,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
number_value as numberValue,
date_value as dateValue,
created_at as createdAt
- from session_data
+ from session_data final
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
order by data_key asc
diff --git a/src/queries/analytics/sessions/getSessionDataProperties.ts b/src/queries/analytics/sessions/getSessionDataProperties.ts
index bfbd9246..b33fecfb 100644
--- a/src/queries/analytics/sessions/getSessionDataProperties.ts
+++ b/src/queries/analytics/sessions/getSessionDataProperties.ts
@@ -52,7 +52,7 @@ async function clickhouseQuery(
select
data_key as propertyName,
count(*) as total
- from session_data
+ from session_data final
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
diff --git a/src/queries/analytics/sessions/getSessionDataValues.ts b/src/queries/analytics/sessions/getSessionDataValues.ts
index 74524204..0f0f806a 100644
--- a/src/queries/analytics/sessions/getSessionDataValues.ts
+++ b/src/queries/analytics/sessions/getSessionDataValues.ts
@@ -51,7 +51,7 @@ async function clickhouseQuery(
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
count(*) as "total"
- from session_data
+ from session_data final
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and data_key = {propertyName:String}
diff --git a/src/queries/analytics/sessions/getWebsiteSessionStats.ts b/src/queries/analytics/sessions/getWebsiteSessionStats.ts
new file mode 100644
index 00000000..5660a5fa
--- /dev/null
+++ b/src/queries/analytics/sessions/getWebsiteSessionStats.ts
@@ -0,0 +1,82 @@
+import clickhouse from 'lib/clickhouse';
+import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
+import prisma from 'lib/prisma';
+import { QueryFilters } from 'lib/types';
+
+export async function getWebsiteSessionStats(
+ ...args: [websiteId: string, filters: QueryFilters]
+): Promise<
+ { pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
+> {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(
+ websiteId: string,
+ filters: QueryFilters,
+): Promise<
+ { pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
+> {
+ const { parseFilters, rawQuery } = prisma;
+ const { filterQuery, params } = await parseFilters(websiteId, {
+ ...filters,
+ });
+
+ return rawQuery(
+ `
+ select
+ count(*) as "pageviews",
+ count(distinct website_event.session_id) as "visitors",
+ count(distinct website_event.visit_id) as "visits",
+ count(distinct session.country) as "countries",
+ sum(case when website_event.event_type = 2 then 1 else 0 end) as "events"
+ from website_event
+ join session on website_event.session_id = session.session_id
+ where website_event.website_id = {{websiteId::uuid}}
+ and website_event.created_at between {{startDate}} and {{endDate}}
+ ${filterQuery}
+ `,
+ params,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ filters: QueryFilters,
+): Promise<
+ { pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
+> {
+ const { rawQuery, parseFilters } = clickhouse;
+ const { filterQuery, params } = await parseFilters(websiteId, {
+ ...filters,
+ });
+
+ return rawQuery(
+ `
+ select
+ sum(views) as "pageviews",
+ uniq(session_id) as "visitors",
+ uniq(visit_id) as "visits",
+ uniq(country) as "countries",
+ sum(length(event_name)) as "events"
+ from umami.website_event_stats_hourly "website_event"
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ ${filterQuery}
+ `,
+ params,
+ ).then(result => {
+ return Object.values(result).map((a: any) => {
+ return {
+ pageviews: Number(a.pageviews),
+ visitors: Number(a.visitors),
+ visits: Number(a.visits),
+ countries: Number(a.countries),
+ events: Number(a.events),
+ };
+ });
+ });
+}
diff --git a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts
index 9031edf5..c92c2929 100644
--- a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts
+++ b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts
@@ -21,7 +21,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
select
${getDateWeeklySQL('created_at')} as time,
count(distinct session_id) as value
- from website_event_stats_hourly
+ from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
group by time