Merge branch 'dev' of https://github.com/umami-software/umami into analytics

This commit is contained in:
Francis Cao 2025-07-17 11:22:21 -07:00
commit 90682b79d2
75 changed files with 3864 additions and 2816 deletions

View file

@ -14,7 +14,7 @@ export async function getEventDataFields(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -29,6 +29,9 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
count(*) as "total"
from event_data
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
${filterQuery}
@ -45,7 +48,7 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -56,7 +59,8 @@ async function clickhouseQuery(
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
count(*) as "total"
from event_data
from event_data website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}

View file

@ -17,7 +17,7 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string },
) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, filters, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
columns: { propertyName: 'data_key' },
});
@ -29,6 +29,9 @@ async function relationalQuery(
count(*) as "total"
from event_data
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
${filterQuery}
@ -45,7 +48,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string },
): Promise<{ eventName: string; propertyName: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
columns: { propertyName: 'data_key' },
});
@ -55,7 +58,8 @@ async function clickhouseQuery(
event_name as eventName,
data_key as propertyName,
count(*) as total
from event_data
from event_data website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}

View file

@ -18,7 +18,7 @@ export async function getEventDataStats(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -32,8 +32,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
data_key,
count(*) as "total"
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by website_event_id, data_key
) as t
@ -47,7 +51,7 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ events: number; properties: number; records: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -60,7 +64,8 @@ async function clickhouseQuery(
event_id,
data_key,
count(*) as "total"
from event_data
from event_data website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}

View file

@ -20,7 +20,7 @@ async function relationalQuery(
filters: QueryFilters & { eventName?: string; propertyName?: string },
) {
const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -33,6 +33,9 @@ async function relationalQuery(
count(*) as "total"
from event_data
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
and event_data.data_key = {{propertyName}}
@ -51,7 +54,7 @@ async function clickhouseQuery(
filters: QueryFilters & { eventName?: string; propertyName?: string },
): Promise<{ value: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -60,7 +63,8 @@ async function clickhouseQuery(
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
count(*) as "total"
from event_data
from event_data website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and data_key = {propertyName:String}

View file

@ -16,7 +16,7 @@ export async function getEventMetrics(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.customEvent,
});
@ -28,6 +28,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} t,
count(*) y
from website_event
${cohortQuery}
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@ -46,20 +47,21 @@ async function clickhouseQuery(
): Promise<{ x: string; t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.customEvent,
});
let sql = '';
if (filterQuery) {
if (filterQuery || cohortQuery) {
sql = `
select
event_name x,
${getDateSQL('created_at', unit, timezone)} t,
count(*) y
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -15,7 +15,7 @@ export function getWebsiteEvents(
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { pagedRawQuery, parseFilters } = prisma;
const { search } = pageParams;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
});
@ -38,6 +38,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
event_type as "eventType",
event_name as "eventName"
from website_event
${cohortQuery}
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
${filterQuery}
@ -56,7 +57,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { pagedQuery, parseFilters } = clickhouse;
const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
const { params, dateQuery, filterQuery, cohortQuery } = await parseFilters(websiteId, filters);
const { search } = pageParams;
return pagedQuery(
@ -75,6 +76,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
event_type as eventType,
event_name as eventName
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}

View file

@ -5,6 +5,7 @@ import kafka from '@/lib/kafka';
import prisma from '@/lib/prisma';
import { uuid } from '@/lib/crypto';
import { saveEventData } from './saveEventData';
import { saveRevenue } from './saveRevenue';
export interface SaveEventArgs {
websiteId: string;
@ -130,6 +131,20 @@ async function relationalQuery({
eventData,
createdAt,
});
const { revenue, currency } = eventData;
if (revenue > 0 && currency) {
await saveRevenue({
websiteId,
sessionId,
eventId: websiteEventId,
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
currency,
revenue,
createdAt,
});
}
}
}

View file

@ -0,0 +1,36 @@
import { uuid } from '@/lib/crypto';
import { PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
export interface SaveRevenueArgs {
websiteId: string;
sessionId: string;
eventId: string;
eventName: string;
currency: string;
revenue: number;
createdAt: Date;
}
export async function saveRevenue(data: SaveRevenueArgs) {
return runQuery({
[PRISMA]: () => relationalQuery(data),
});
}
async function relationalQuery(data: SaveRevenueArgs) {
const { websiteId, sessionId, eventId, eventName, currency, revenue, createdAt } = data;
await prisma.client.revenue.create({
data: {
id: uuid(),
websiteId,
sessionId,
eventId,
eventName,
currency,
revenue,
createdAt,
},
});
}

View file

@ -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 { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -21,6 +21,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
url_query as query,
count(distinct session_id) as visitors
from website_event
${cohortQuery}
where website_id = {{websiteId::uuid}}
${filterQuery}
${dateQuery}
@ -36,7 +37,7 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
const sql = `
select
@ -44,6 +45,7 @@ async function clickhouseQuery(
url_query as query,
uniq(session_id) as visitors
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${filterQuery}
${dateQuery}

View file

@ -12,7 +12,7 @@ export async function getRealtimeActivity(...args: [websiteId: string, filters:
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -27,6 +27,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
website_event.url_path as "urlPath",
website_event.referrer_domain as "referrerDomain"
from website_event
${cohortQuery}
inner join session
on session.session_id = website_event.session_id
where website_event.website_id = {{websiteId::uuid}}
@ -41,7 +42,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> {
const { rawQuery, parseFilters } = clickhouse;
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -56,6 +57,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
url_path as urlPath,
referrer_domain as referrerDomain
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${filterQuery}
${dateQuery}

View file

@ -21,6 +21,12 @@ async function relationalQuery(
const { rawQuery, getSearchSQL } = prisma;
const params = {};
let searchQuery = '';
let excludeDomain = '';
if (column === 'referrer_domain') {
excludeDomain = `and website_event.referrer_domain != website_event.hostname
and website_event.referrer_domain != ''`;
}
if (search) {
if (decodeURIComponent(search).includes(',')) {
@ -49,6 +55,7 @@ async function relationalQuery(
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${searchQuery}
${excludeDomain}
group by 1
order by 2 desc
limit 10
@ -73,6 +80,11 @@ async function clickhouseQuery(
const { rawQuery, getSearchSQL } = clickhouse;
const params = {};
let searchQuery = '';
let excludeDomain = '';
if (column === 'referrer_domain') {
excludeDomain = `and referrer_domain != hostname and referrer_domain != ''`;
}
if (search) {
searchQuery = `and positionCaseInsensitive(${column}, {search:String}) > 0`;
@ -103,6 +115,7 @@ async function clickhouseQuery(
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${searchQuery}
${excludeDomain}
group by 1
order by 2 desc
limit 10

View file

@ -23,7 +23,7 @@ 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, {
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -44,6 +44,7 @@ async function relationalQuery(
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${cohortQuery}
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@ -63,7 +64,7 @@ async function clickhouseQuery(
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -86,6 +87,7 @@ async function clickhouseQuery(
min(created_at) min_time,
max(created_at) max_time
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
@ -107,7 +109,8 @@ async function clickhouseQuery(
sum(views) c,
min(min_time) min_time,
max(max_time) max_time
from umami.website_event_stats_hourly "website_event"
from website_event_stats_hourly "website_event"
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -28,13 +28,13 @@ async function relationalQuery(
) {
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
websiteId,
{
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
},
{ joinSession: SESSION_COLUMNS.includes(type) || column === 'referrer_domain' },
{ joinSession: SESSION_COLUMNS.includes(type) },
);
let entryExitQuery = '';
@ -66,8 +66,9 @@ async function relationalQuery(
return rawQuery(
`
select ${column} x,
${column === 'referrer_domain' ? 'count(distinct website_event.session_id)' : 'count(*)'} as y
count(distinct website_event.session_id) as y
from website_event
${cohortQuery}
${joinSession}
${entryExitQuery}
where website_event.website_id = {{websiteId::uuid}}
@ -93,7 +94,7 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
});
@ -125,8 +126,9 @@ async function clickhouseQuery(
sql = `
select ${column} x,
${column === 'referrer_domain' ? 'uniq(session_id)' : 'count(*)'} as y
uniq(website_event.session_id) as y
from website_event
${cohortQuery}
${entryExitQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
@ -143,28 +145,29 @@ async function clickhouseQuery(
let columnQuery = `arrayJoin(${column})`;
if (column === 'referrer_domain') {
excludeDomain = `and t != hostname and t != ''`;
columnQuery = `session_id s, arrayJoin(${column})`;
excludeDomain = `and t != ''`;
}
if (type === 'entry') {
columnQuery = `visit_id x, argMinMerge(entry_url)`;
columnQuery = `argMinMerge(entry_url)`;
}
if (type === 'exit') {
columnQuery = `visit_id x, argMaxMerge(exit_url)`;
columnQuery = `argMaxMerge(exit_url)`;
}
if (type === 'entry' || type === 'exit') {
groupByQuery = 'group by x';
groupByQuery = 'group by s';
}
sql = `
select g.t as x,
${column === 'referrer_domain' ? 'uniq(s)' : 'count(*)'} as y
uniq(s) as y
from (
select ${columnQuery} as t
select session_id s,
${columnQuery} as t
from website_event_stats_hourly website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -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, joinSession, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -25,6 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} x,
count(*) y
from website_event
${cohortQuery}
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@ -43,7 +44,7 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> {
const { timezone = 'utc', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -60,6 +61,7 @@ async function clickhouseQuery(
${getDateSQL('website_event.created_at', unit, timezone)} as t,
count(*) as y
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
@ -78,6 +80,7 @@ async function clickhouseQuery(
${getDateSQL('website_event.created_at', unit, timezone)} as t,
sum(views)as y
from website_event_stats_hourly website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -78,26 +78,16 @@ async function relationalQuery(
group by 1),`;
const revenueEventQuery = `WITH events AS (
select
we.session_id,
max(ed.created_at) max_dt,
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) value
from event_data ed
join website_event we
on we.event_id = ed.website_event_id
and we.website_id = ed.website_id
join (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and data_key ${like} '%currency%'
and string_value = {{currency}}) currency
on currency.website_event_id = ed.website_event_id
where ed.website_id = {{websiteId::uuid}}
and ed.created_at between {{startDate}} and {{endDate}}
and ${column} = {{conversionStep}}
and ed.data_key ${like} '%revenue%'
group by 1),`;
select
session_id,
max(created_at) max_dt,
sum(revenue) value
from revenue
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and ${column} = {{conversionStep}}
and currency ${like} {{currency}}
group by 1),`;
function getModelQuery(model: string) {
return model === 'firstClick'

View file

@ -228,7 +228,7 @@ async function clickhouseQuery(
`
WITH level0 AS (
select distinct session_id, url_path, referrer_path, event_name, created_at
from umami.website_event
from website_event
where (${stepFilterQuery})
and website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}

View file

@ -24,7 +24,7 @@ async function relationalQuery(
}[]
> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
websiteId,
{
...filters,
@ -53,6 +53,7 @@ async function relationalQuery(
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${cohortQuery}
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@ -80,7 +81,7 @@ async function clickhouseQuery(
}[]
> {
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -103,6 +104,7 @@ async function clickhouseQuery(
min(created_at) min_time,
max(created_at) max_time
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -229,7 +229,7 @@ async function clickhouseQuery(
visit_id,
coalesce(nullIf(event_name, ''), url_path) event,
row_number() OVER (PARTITION BY visit_id ORDER BY created_at) AS event_number
from umami.website_event
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}),
${sequenceQuery}

View file

@ -48,22 +48,13 @@ async function relationalQuery(
const chartRes = await rawQuery(
`
select
we.event_name x,
${getDateSQL('ed.created_at', unit, timezone)} t,
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) y
from event_data ed
join website_event we
on we.event_id = ed.website_event_id
join (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and data_key ${like} '%currency%'
and string_value = {{currency}}) currency
on currency.website_event_id = ed.website_event_id
where ed.website_id = {{websiteId::uuid}}
and ed.created_at between {{startDate}} and {{endDate}}
and ed.data_key ${like} '%revenue%'
event_name x,
${getDateSQL('created_at', unit, timezone)} t,
sum(revenue) y
from revenue
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and currency ${like} {{currency}}
group by x, t
order by t
`,
@ -74,22 +65,13 @@ async function relationalQuery(
`
select
s.country as name,
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) value
from event_data ed
join website_event we
on we.event_id = ed.website_event_id
sum(r.revenue) value
from revenue r
join session s
on s.session_id = we.session_id
join (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and data_key ${like} '%currency%'
and string_value = {{currency}}) currency
on currency.website_event_id = ed.website_event_id
where ed.website_id = {{websiteId::uuid}}
and ed.created_at between {{startDate}} and {{endDate}}
and ed.data_key ${like} '%revenue%'
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}}
group by s.country
`,
{ websiteId, startDate, endDate, currency },
@ -98,22 +80,13 @@ async function relationalQuery(
const totalRes = await rawQuery(
`
select
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) as sum,
sum(revenue) as sum,
count(distinct event_id) as count,
count(distinct session_id) as unique_count
from event_data ed
join website_event we
on we.event_id = ed.website_event_id
join (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and data_key ${like} '%currency%'
and string_value = {{currency}}) currency
on currency.website_event_id = ed.website_event_id
where ed.website_id = {{websiteId::uuid}}
and ed.created_at between {{startDate}} and {{endDate}}
and ed.data_key ${like} '%revenue%'
from revenue r
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and currency ${like} {{currency}}
`,
{ websiteId, startDate, endDate, currency },
).then(result => result?.[0]);
@ -121,24 +94,15 @@ async function relationalQuery(
const tableRes = await rawQuery(
`
select
c.currency,
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) as sum,
count(distinct ed.website_event_id) as count,
count(distinct we.session_id) as unique_count
from event_data ed
join website_event we
on we.event_id = ed.website_event_id
join (select website_event_id, string_value as currency
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and data_key ${like} '%currency%') c
on c.website_event_id = ed.website_event_id
where ed.website_id = {{websiteId::uuid}}
and ed.created_at between {{startDate}} and {{endDate}}
and ed.data_key ${like} '%revenue%'
group by c.currency
order by sum desc;
currency,
sum(revenue) as sum,
count(distinct event_id) as count,
count(distinct session_id) as unique_count
from revenue r
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
group by currency
order by sum desc
`,
{ websiteId, startDate, endDate, unit, timezone, currency },
);

View file

@ -1,7 +1,10 @@
import { Prisma } from '@prisma/client';
import prisma from '@/lib/prisma';
export async function createSession(data: Prisma.SessionCreateInput) {
export async function createSession(
data: Prisma.SessionCreateInput,
options = { skipDuplicates: false },
) {
const {
id,
websiteId,
@ -16,19 +19,31 @@ export async function createSession(data: Prisma.SessionCreateInput) {
distinctId,
} = data;
return prisma.client.session.create({
data: {
id,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId,
},
});
try {
return await prisma.client.session.create({
data: {
id,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId,
},
});
} catch (e: any) {
// With skipDuplicates flag: ignore unique constraint error and return null
if (
options.skipDuplicates &&
e instanceof Prisma.PrismaClientKnownRequestError &&
e.code === 'P2002'
) {
return null;
}
throw e;
}
}

View file

@ -17,7 +17,7 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string },
) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, filters, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
columns: { propertyName: 'data_key' },
});
@ -25,12 +25,13 @@ async function relationalQuery(
`
select
data_key as "propertyName",
count(distinct d.session_id) as "total"
from website_event e
join session_data d
on d.session_id = e.session_id
where e.website_id = {{websiteId::uuid}}
and e.created_at between {{startDate}} and {{endDate}}
count(distinct session_data.session_id) as "total"
from website_event
${cohortQuery}
join session_data
on session_data.session_id = website_event.session_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by 1
order by 2 desc
@ -45,7 +46,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string },
): Promise<{ propertyName: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
columns: { propertyName: 'data_key' },
});
@ -53,13 +54,14 @@ async function clickhouseQuery(
`
select
data_key as propertyName,
count(distinct d.session_id) as total
from website_event e
join session_data d final
on d.session_id = e.session_id
where e.website_id = {websiteId:UUID}
and e.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and d.data_key != ''
count(distinct session_data.session_id) as total
from website_event
${cohortQuery}
join session_data final
on session_data.session_id = website_event.session_id
where website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and session_data.data_key != ''
${filterQuery}
group by 1
order by 2 desc

View file

@ -17,7 +17,7 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string },
) {
const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -27,13 +27,14 @@ async function relationalQuery(
when data_type = 4 then ${getDateSQL('date_value', 'hour')}
else string_value
end as "value",
count(distinct d.session_id) as "total"
count(distinct session_data.session_id) as "total"
from website_event e
${cohortQuery}
join session_data d
on d.session_id = e.session_id
where e.website_id = {{websiteId::uuid}}
and e.created_at between {{startDate}} and {{endDate}}
and d.data_key = {{propertyName}}
on session_data.session_id = website_event.session_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and session_data.data_key = {{propertyName}}
${filterQuery}
group by value
order by 2 desc
@ -48,7 +49,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string },
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
@ -56,13 +57,14 @@ async function clickhouseQuery(
multiIf(data_type = 2, replaceAll(string_value, '.0000', ''),
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
uniq(d.session_id) as "total"
uniq(session_data.session_id) as "total"
from website_event e
${cohortQuery}
join session_data d final
on d.session_id = e.session_id
where e.website_id = {websiteId:UUID}
and e.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and d.data_key = {propertyName:String}
on session_data.session_id = website_event.session_id
where website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and session_data.data_key = {propertyName:String}
${filterQuery}
group by value
order by 2 desc

View file

@ -28,7 +28,7 @@ async function relationalQuery(
) {
const column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
websiteId,
{
...filters,
@ -47,6 +47,7 @@ async function relationalQuery(
count(distinct website_event.session_id) y
${includeCountry ? ', country' : ''}
from website_event
${cohortQuery}
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@ -71,7 +72,7 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -86,6 +87,7 @@ async function clickhouseQuery(
count(distinct session_id) y
${includeCountry ? ', country' : ''}
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
@ -103,6 +105,7 @@ async function clickhouseQuery(
uniq(session_id) y
${includeCountry ? ', country' : ''}
from website_event_stats_hourly website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -14,7 +14,7 @@ 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, joinSession, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -25,6 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} x,
count(distinct website_event.session_id) y
from website_event
${cohortQuery}
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@ -43,7 +44,7 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> {
const { timezone = 'utc', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -60,6 +61,7 @@ async function clickhouseQuery(
${getDateSQL('website_event.created_at', unit, timezone)} as t,
count(distinct session_id) as y
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
@ -78,6 +80,7 @@ async function clickhouseQuery(
${getDateSQL('website_event.created_at', unit, timezone)} as t,
uniq(session_id) as y
from website_event_stats_hourly website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -17,7 +17,6 @@ async function relationalQuery(websiteId: string, sessionId: string) {
select id,
distinct_id as "distinctId",
website_id as "websiteId",
hostname,
browser,
os,
device,
@ -37,7 +36,6 @@ async function relationalQuery(websiteId: string, sessionId: string) {
session.distinct_id,
website_event.visit_id,
session.website_id,
website_event.hostname,
session.browser,
session.os,
session.device,
@ -54,8 +52,8 @@ async function relationalQuery(websiteId: string, sessionId: string) {
join website_event on website_event.session_id = session.session_id
where session.website_id = {{websiteId::uuid}}
and session.session_id = {{sessionId::uuid}}
group by session.session_id, session.distinct_id, visit_id, session.website_id, website_event.hostname, session.browser, session.os, session.device, session.screen, session.language, session.country, session.region, session.city) t
group by id, distinct_id, website_id, hostname, browser, os, device, screen, language, country, region, city;
group by session.session_id, session.distinct_id, visit_id, session.website_id, session.browser, session.os, session.device, session.screen, session.language, session.country, session.region, session.city) t
group by id, distinct_id, website_id, browser, os, device, screen, language, country, region, city;
`,
{ websiteId, sessionId },
).then(result => result?.[0]);
@ -69,7 +67,6 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
select id,
websiteId,
distinctId,
hostname,
browser,
os,
device,
@ -89,7 +86,6 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
distinct_id as distinctId,
visit_id,
website_id as websiteId,
hostname,
browser,
os,
device,
@ -105,8 +101,8 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
from website_event_stats_hourly
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
group by session_id, distinct_id, visit_id, website_id, hostname, browser, os, device, screen, language, country, region, city) t
group by id, websiteId, distinctId, hostname, browser, os, device, screen, language, country, region, city;
group by session_id, distinct_id, visit_id, website_id, browser, os, device, screen, language, country, region, city) t
group by id, websiteId, distinctId, browser, os, device, screen, language, country, region, city;
`,
{ websiteId, sessionId },
).then(result => result?.[0]);

View file

@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
@ -21,7 +22,7 @@ async function relationalQuery(
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
> {
const { parseFilters, rawQuery } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
});
@ -34,6 +35,7 @@ async function relationalQuery(
count(distinct session.country) as "countries",
sum(case when website_event.event_type = 2 then 1 else 0 end) as "events"
from website_event
${cohortQuery}
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}}
@ -50,23 +52,41 @@ async function clickhouseQuery(
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
});
return rawQuery(
`
let sql = '';
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
sql = `
select
sumIf(1, event_type = 1) as "pageviews",
uniq(session_id) as "visitors",
uniq(visit_id) as "visits",
uniq(country) as "countries",
sum(length(event_name)) as "events"
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
`;
} else {
sql = `
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"
from website_event_stats_hourly website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
`,
params,
);
`;
}
return rawQuery(sql, params);
}

View file

@ -1,4 +1,5 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { PageParams, QueryFilters } from '@/lib/types';
@ -15,7 +16,7 @@ export async function getWebsiteSessions(
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams: PageParams) {
const { pagedRawQuery, parseFilters } = prisma;
const { search } = pageParams;
const { filterQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
});
@ -27,7 +28,6 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
select
session.session_id as "id",
session.website_id as "websiteId",
website_event.hostname,
session.browser,
session.os,
session.device,
@ -42,6 +42,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
sum(case when website_event.event_type = 1 then 1 else 0 end) as "views",
max(website_event.created_at) as "createdAt"
from website_event
${cohortQuery}
join session on session.session_id = website_event.session_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@ -57,7 +58,6 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
}
group by session.session_id,
session.website_id,
website_event.hostname,
session.browser,
session.os,
session.device,
@ -75,15 +75,51 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { pagedQuery, parseFilters, getDateStringSQL } = clickhouse;
const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
const { params, dateQuery, filterQuery, cohortQuery } = await parseFilters(websiteId, filters);
const { search } = pageParams;
return pagedQuery(
`
let sql = '';
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
sql = `
select
session_id as id,
website_id as websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
${getDateStringSQL('min(created_at)')} as firstAt,
${getDateStringSQL('max(created_at)')} as lastAt,
uniq(visit_id) as visits,
sumIf(views, event_type = 1) as views,
lastAt as createdAt
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}
${
search
? `and ((positionCaseInsensitive(distinct_id, {search:String}) > 0)
or (positionCaseInsensitive(city, {search:String}) > 0)
or (positionCaseInsensitive(browser, {search:String}) > 0)
or (positionCaseInsensitive(os, {search:String}) > 0)
or (positionCaseInsensitive(device, {search:String}) > 0))`
: ''
}
group by session_id, website_id, browser, os, device, screen, language, country, region, city
order by lastAt desc
`;
} else {
sql = `
select
session_id as id,
website_id as websiteId,
hostname,
browser,
os,
device,
@ -97,7 +133,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
uniq(visit_id) as visits,
sumIf(views, event_type = 1) as views,
lastAt as createdAt
from website_event_stats_hourly
from website_event_stats_hourly website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}
@ -110,10 +147,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
or (positionCaseInsensitive(device, {search:String}) > 0))`
: ''
}
group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
group by session_id, website_id, browser, os, device, screen, language, country, region, city
order by lastAt desc
`,
{ ...params, search },
pageParams,
);
`;
}
return pagedQuery(sql, { ...params, search }, pageParams);
}

View file

@ -2,6 +2,7 @@ import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
import { EVENT_COLUMNS } from '@/lib/constants';
export async function getWebsiteSessionsWeekly(
...args: [websiteId: string, filters?: QueryFilters]
@ -35,21 +36,39 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc' } = filters;
const { rawQuery, parseFilters } = clickhouse;
const { params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
let sql = '';
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
sql = `
select
formatDateTime(toDateTime(created_at, '${timezone}'), '%w:%H') as time,
count(distinct session_id) as value
from website_event_stats_hourly
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by time
order by time
`,
params,
).then(formatResults);
`;
} else {
sql = `
select
formatDateTime(toDateTime(created_at, '${timezone}'), '%w:%H') as time,
count(distinct session_id) as value
from website_event_stats_hourly website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by time
order by time
`;
}
return rawQuery(sql, params).then(formatResults);
}
function formatResults(data: any) {