Moved code into src folder. Added build for component library.

This commit is contained in:
Mike Cao 2023-08-21 02:06:09 -07:00
parent 7a7233ead4
commit ede658771e
490 changed files with 749 additions and 442 deletions

View file

@ -0,0 +1,104 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import { QueryFilters, WebsiteEventData } from 'lib/types';
export async function getEventDataEvents(
...args: [websiteId: string, filters: QueryFilters]
): Promise<WebsiteEventData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { event } = filters;
const { params } = await parseFilters(websiteId, filters);
if (event) {
return rawQuery(
`
select
website_event.event_name as "eventName",
event_data.event_key as "fieldName",
event_data.data_type as "dataType",
event_data.string_value as "fieldValue",
count(*) as "total"
from event_data
inner join website_event
on website_event.event_id = event_data.website_event_id
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
and website_event.event_name = {{event}}
group by website_event.event_name, event_data.event_key, event_data.data_type, event_data.string_value
order by 1 asc, 2 asc, 3 asc, 4 desc
`,
params,
);
}
return rawQuery(
`
select
website_event.event_name as "eventName",
event_data.event_key as "fieldName",
event_data.data_type as "dataType",
count(*) as "total"
from event_data
inner join website_event
on website_event.event_id = event_data.website_event_id
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
group by website_event.event_name, event_data.event_key, event_data.data_type
order by 1 asc, 2 asc
limit 100
`,
params,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = clickhouse;
const { event } = filters;
const { params } = await parseFilters(websiteId, filters);
if (event) {
return rawQuery(
`
select
event_name as eventName,
event_key as fieldName,
data_type as dataType,
string_value as fieldValue,
count(*) as total
from event_data
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_name = {event:String}
group by event_key, data_type, string_value, event_name
order by 1 asc, 2 asc, 3 asc, 4 desc
limit 100
`,
params,
);
}
return rawQuery(
`
select
event_name as eventName,
event_key as fieldName,
data_type as dataType,
count(*) as total
from event_data
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
group by event_key, data_type, event_name
order by 1 asc, 2 asc
limit 100
`,
params,
);
}

View file

@ -0,0 +1,63 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import { QueryFilters, WebsiteEventData } from 'lib/types';
export async function getEventDataFields(
...args: [websiteId: string, filters: QueryFilters & { field?: string }]
): Promise<WebsiteEventData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters & { field?: string }) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, filters, {
columns: { field: 'event_key' },
});
return rawQuery(
`
select
event_key as "fieldName",
data_type as "dataType",
string_value as "fieldValue",
count(*) as "total"
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by event_key, data_type, string_value
order by 3 desc, 2 desc, 1 asc
limit 100
`,
params,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters & { field?: string }) {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters, {
columns: { field: 'event_key' },
});
return rawQuery(
`
select
event_key as fieldName,
data_type as dataType,
string_value as fieldValue,
count(*) as total
from event_data
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
${filterQuery}
group by event_key, data_type, string_value
order by 3 desc, 2 desc, 1 asc
limit 100
`,
params,
);
}

View file

@ -0,0 +1,69 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import { QueryFilters } from 'lib/types';
export async function getEventDataStats(
...args: [websiteId: string, filters: QueryFilters]
): Promise<{
events: number;
fields: number;
records: number;
}> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
}).then(results => results[0]);
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
select
count(distinct t.website_event_id) as "events",
count(distinct t.event_key) as "fields",
sum(t.total) as "records"
from (
select
website_event_id,
event_key,
count(*) as "total"
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by website_event_id, event_key
) as t
`,
params,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters);
return rawQuery(
`
select
count(distinct t.event_id) as "events",
count(distinct t.event_key) as "fields",
sum(t.total) as "records"
from (
select
event_id,
event_key,
count(*) as "total"
from event_data
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
${filterQuery}
group by event_id, event_key
) as t
`,
params,
);
}

View file

@ -0,0 +1,30 @@
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from 'lib/db';
export function getEventDataUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) {
return runQuery({
[PRISMA]: notImplemented,
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
function clickhouseQuery(websiteIds: string[], startDate: Date, endDate: Date) {
const { rawQuery } = clickhouse;
return rawQuery(
`
select
website_id as websiteId,
count(*) as count
from event_data
where created_at between {startDate:DateTime64} and {endDate:DateTime64}
and website_id in {websiteIds:Array(UUID)}
group by website_id
`,
{
websiteIds,
startDate,
endDate,
},
);
}

View file

@ -0,0 +1,91 @@
import { Prisma } from '@prisma/client';
import { DATA_TYPE } from 'lib/constants';
import { uuid } from 'lib/crypto';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import { flattenJSON } from 'lib/data';
import kafka from 'lib/kafka';
import prisma from 'lib/prisma';
import { DynamicData } from 'lib/types';
export async function saveEventData(args: {
websiteId: string;
eventId: string;
sessionId?: string;
urlPath?: string;
eventName?: string;
eventData: DynamicData;
createdAt?: string;
}) {
return runQuery({
[PRISMA]: () => relationalQuery(args),
[CLICKHOUSE]: () => clickhouseQuery(args),
});
}
async function relationalQuery(data: {
websiteId: string;
eventId: string;
eventData: DynamicData;
}): Promise<Prisma.BatchPayload> {
const { websiteId, eventId, eventData } = data;
const jsonKeys = flattenJSON(eventData);
// id, websiteEventId, eventStringValue
const flattendData = jsonKeys.map(a => ({
id: uuid(),
websiteEventId: eventId,
websiteId,
eventKey: a.key,
stringValue:
a.dynamicDataType === DATA_TYPE.number
? parseFloat(a.value).toFixed(4)
: a.dynamicDataType === DATA_TYPE.date
? a.value.split('.')[0] + 'Z'
: a.value.toString(),
numberValue: a.dynamicDataType === DATA_TYPE.number ? a.value : null,
dateValue: a.dynamicDataType === DATA_TYPE.date ? new Date(a.value) : null,
dataType: a.dynamicDataType,
}));
return prisma.client.eventData.createMany({
data: flattendData,
});
}
async function clickhouseQuery(data: {
websiteId: string;
eventId: string;
sessionId?: string;
urlPath?: string;
eventName?: string;
eventData: DynamicData;
createdAt?: string;
}) {
const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data;
const { getDateFormat, sendMessages } = kafka;
const jsonKeys = flattenJSON(eventData);
const messages = jsonKeys.map(a => ({
website_id: websiteId,
session_id: sessionId,
event_id: eventId,
url_path: urlPath,
event_name: eventName,
event_key: a.key,
string_value:
a.dynamicDataType === DATA_TYPE.date
? getDateFormat(a.value, 'isoUtcDateTime')
: a.value.toString(),
number_value: a.dynamicDataType === DATA_TYPE.number ? a.value : null,
date_value: a.dynamicDataType === DATA_TYPE.date ? getDateFormat(a.value) : null,
data_type: a.dynamicDataType,
created_at: createdAt,
}));
await sendMessages(messages, 'event_data');
return data;
}

View file

@ -0,0 +1,67 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
import { WebsiteEventMetric, QueryFilters } from 'lib/types';
import { EVENT_TYPE } from 'lib/constants';
export async function getEventMetrics(
...args: [websiteId: string, filters: QueryFilters]
): Promise<WebsiteEventMetric[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateQuery, parseFilters } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.customEvent,
});
return rawQuery(
`
select
event_name x,
${getDateQuery('created_at', unit, timezone)} t,
count(*) y
from website_event
${joinSession}
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1, 2
order by 2
`,
params,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.customEvent,
});
return rawQuery(
`
select
event_name x,
${getDateQuery('created_at', unit, timezone)} t,
count(*) y
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_type = {eventType:UInt32}
${filterQuery}
group by x, t
order by t
`,
params,
);
}

View file

@ -0,0 +1,30 @@
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from 'lib/db';
export function getEventUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) {
return runQuery({
[PRISMA]: notImplemented,
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
function clickhouseQuery(websiteIds: string[], startDate: Date, endDate: Date) {
const { rawQuery } = clickhouse;
return rawQuery(
`
select
website_id as websiteId,
count(*) as count
from website_event
where website_id in {websiteIds:Array(UUID)}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
group by website_id
`,
{
websiteIds,
startDate,
endDate,
},
);
}

View file

@ -0,0 +1,49 @@
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
export function getEvents(...args: [websiteId: string, startDate: Date, eventType: number]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
function relationalQuery(websiteId: string, startDate: Date, eventType: number) {
return prisma.client.websiteEvent.findMany({
where: {
websiteId,
eventType,
createdAt: {
gte: startDate,
},
},
});
}
function clickhouseQuery(websiteId: string, startDate: Date, eventType: number) {
const { rawQuery } = clickhouse;
return rawQuery(
`
select
event_id as id,
website_id as websiteId,
session_id as sessionId,
created_at as createdAt,
toUnixTimestamp(created_at) as timestamp,
url_path as urlPath,
referrer_domain as referrerDomain,
event_name as eventName
from website_event
where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime}
and event_type = {eventType:UInt32}
`,
{
websiteId,
startDate,
eventType,
},
);
}

View file

@ -0,0 +1,170 @@
import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE } from 'lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import kafka from 'lib/kafka';
import prisma from 'lib/prisma';
import { uuid } from 'lib/crypto';
import { saveEventData } from 'queries/analytics/eventData/saveEventData';
export async function saveEvent(args: {
sessionId: string;
websiteId: string;
urlPath: string;
urlQuery?: string;
referrerPath?: string;
referrerQuery?: string;
referrerDomain?: string;
pageTitle?: string;
eventName?: string;
eventData?: any;
hostname?: string;
browser?: string;
os?: string;
device?: string;
screen?: string;
language?: string;
country?: string;
subdivision1?: string;
subdivision2?: string;
city?: string;
}) {
return runQuery({
[PRISMA]: () => relationalQuery(args),
[CLICKHOUSE]: () => clickhouseQuery(args),
});
}
async function relationalQuery(data: {
sessionId: string;
websiteId: string;
urlPath: string;
urlQuery?: string;
referrerPath?: string;
referrerQuery?: string;
referrerDomain?: string;
pageTitle?: string;
eventName?: string;
eventData?: any;
}) {
const {
websiteId,
sessionId,
urlPath,
urlQuery,
referrerPath,
referrerQuery,
referrerDomain,
eventName,
eventData,
pageTitle,
} = data;
const websiteEventId = uuid();
const websiteEvent = prisma.client.websiteEvent.create({
data: {
id: websiteEventId,
websiteId,
sessionId,
urlPath: urlPath?.substring(0, URL_LENGTH),
urlQuery: urlQuery?.substring(0, URL_LENGTH),
referrerPath: referrerPath?.substring(0, URL_LENGTH),
referrerQuery: referrerQuery?.substring(0, URL_LENGTH),
referrerDomain: referrerDomain?.substring(0, URL_LENGTH),
pageTitle,
eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
},
});
if (eventData) {
await saveEventData({
websiteId,
sessionId,
eventId: websiteEventId,
urlPath: urlPath?.substring(0, URL_LENGTH),
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
eventData,
});
}
return websiteEvent;
}
async function clickhouseQuery(data: {
sessionId: string;
websiteId: string;
urlPath: string;
urlQuery?: string;
referrerPath?: string;
referrerQuery?: string;
referrerDomain?: string;
pageTitle?: string;
eventName?: string;
eventData?: any;
hostname?: string;
browser?: string;
os?: string;
device?: string;
screen?: string;
language?: string;
country?: string;
subdivision1?: string;
subdivision2?: string;
city?: string;
}) {
const {
websiteId,
sessionId,
urlPath,
urlQuery,
referrerPath,
referrerQuery,
referrerDomain,
pageTitle,
eventName,
eventData,
country,
subdivision1,
subdivision2,
city,
...args
} = data;
const { getDateFormat, sendMessage } = kafka;
const eventId = uuid();
const createdAt = getDateFormat(new Date());
const message = {
...args,
website_id: websiteId,
session_id: sessionId,
event_id: uuid(),
country: country ? country : null,
subdivision1: country && subdivision1 ? `${country}-${subdivision1}` : null,
subdivision2: subdivision2 ? subdivision2 : null,
city: city ? city : null,
url_path: urlPath?.substring(0, URL_LENGTH),
url_query: urlQuery?.substring(0, URL_LENGTH),
referrer_path: referrerPath?.substring(0, URL_LENGTH),
referrer_query: referrerQuery?.substring(0, URL_LENGTH),
referrer_domain: referrerDomain?.substring(0, URL_LENGTH),
page_title: pageTitle,
event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
created_at: createdAt,
};
await sendMessage(message, 'event');
if (eventData) {
await saveEventData({
websiteId,
sessionId,
eventId,
urlPath: urlPath?.substring(0, URL_LENGTH),
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
eventData,
createdAt,
});
}
return data;
}

View file

@ -0,0 +1,40 @@
import { subMinutes } from 'date-fns';
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
export async function getActiveVisitors(...args: [websiteId: string]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string) {
const { rawQuery } = prisma;
return rawQuery(
`
select count(distinct session_id) x
from website_event
where website_id = {{websiteId::uuid}}
and created_at >= {{startAt}}
`,
{ websiteId, startAt: subMinutes(new Date(), 5) },
);
}
async function clickhouseQuery(websiteId: string) {
const { rawQuery } = clickhouse;
return rawQuery(
`
select
count(distinct session_id) x
from website_event
where website_id = {websiteId:UUID}
and created_at >= {startAt:DateTime}
`,
{ websiteId, startAt: subMinutes(new Date(), 5) },
);
}

View file

@ -0,0 +1,27 @@
import { md5 } from 'next-basics';
import { getSessions, getEvents } from 'queries/index';
import { EVENT_TYPE } from 'lib/constants';
export async function getRealtimeData(websiteId, time) {
const [pageviews, sessions, events] = await Promise.all([
getEvents(websiteId, time, EVENT_TYPE.pageView),
getSessions(websiteId, time),
getEvents(websiteId, time, EVENT_TYPE.customEvent),
]);
const decorate = (id, data) => {
return data.map(props => ({
...props,
__id: md5(id, ...Object.values(props)),
__type: id,
timestamp: props.timestamp ? props.timestamp * 1000 : new Date(props.createdAt).getTime(),
}));
};
return {
pageviews: decorate('pageview', pageviews),
sessions: decorate('session', sessions),
events: decorate('event', events),
timestamp: Date.now(),
};
}

View file

@ -0,0 +1,38 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
export async function getValues(...args: [websiteId: string, column: string]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, column: string) {
const { rawQuery } = prisma;
return rawQuery(
`
select distinct ${column} as "value"
from website_event
inner join session
on session.session_id = website_event.session_id
where website_event.website_id = {{websiteId::uuid}}
`,
{ websiteId },
);
}
async function clickhouseQuery(websiteId: string, column: string) {
const { rawQuery } = clickhouse;
return rawQuery(
`
select distinct ${column} as value
from website_event
where website_id = {websiteId:UUID}
`,
{ websiteId },
);
}

View file

@ -0,0 +1,49 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
import { DEFAULT_RESET_DATE } from 'lib/constants';
export async function getWebsiteDateRange(...args: [websiteId: string]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string) {
const { rawQuery, parseFilters } = prisma;
const { params } = await parseFilters(websiteId, { startDate: new Date(DEFAULT_RESET_DATE) });
const result = await rawQuery(
`
select
min(created_at) as mindate,
max(created_at) as maxdate
from website_event
where website_id = {{websiteId::uuid}}
and created_at >= {{startDate}}
`,
params,
);
return result[0] ?? null;
}
async function clickhouseQuery(websiteId: string) {
const { rawQuery, parseFilters } = clickhouse;
const { params } = await parseFilters(websiteId, { startDate: new Date(DEFAULT_RESET_DATE) });
const result = await rawQuery(
`
select
min(created_at) as mindate,
max(created_at) as maxdate
from website_event
where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime}
`,
params,
);
return result[0] ?? null;
}

View file

@ -0,0 +1,80 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
import { EVENT_TYPE } from 'lib/constants';
import { QueryFilters } from 'lib/types';
export async function getWebsiteStats(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { getDateQuery, getTimestampIntervalQuery, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select
sum(t.c) as "pageviews",
count(distinct t.session_id) as "uniques",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(t.time) as "totaltime"
from (
select
website_event.session_id,
${getDateQuery('website_event.created_at', 'hour')},
count(*) as c,
${getTimestampIntervalQuery('website_event.created_at')} as "time"
from website_event
join website
on website_event.website_id = website.website_id
${joinSession}
where website.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1, 2
) as t
`,
params,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, getDateQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select
sum(t.c) as "pageviews",
count(distinct t.session_id) as "uniques",
sum(if(t.c = 1, 1, 0)) as "bounces",
sum(if(max_time < min_time + interval 1 hour, max_time-min_time, 0)) as "totaltime"
from (
select
session_id,
${getDateQuery('created_at', 'day')} time_series,
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:DateTime} and {endDate:DateTime}
and event_type = {eventType:UInt32}
${filterQuery}
group by session_id, time_series
) as t;
`,
params,
);
}

View file

@ -0,0 +1,65 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants';
import { QueryFilters } from 'lib/types';
export async function getPageviewMetrics(
...args: [websiteId: string, columns: string, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(
websiteId,
{
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
},
{ joinSession: SESSION_COLUMNS.includes(column) },
);
return rawQuery(
`
select ${column} x, count(*) y
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 1
order by 2 desc
limit 100
`,
params,
);
}
async function clickhouseQuery(websiteId: string, column: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
});
return rawQuery(
`
select ${column} x, count(*) y
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_type = {eventType:UInt32}
${filterQuery}
group by x
order by y desc
limit 100
`,
params,
);
}

View file

@ -0,0 +1,67 @@
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
import { EVENT_TYPE } from 'lib/constants';
import { QueryFilters } from 'lib/types';
export async function getPageviewStats(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { getDateQuery, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select
${getDateQuery('website_event.created_at', unit, timezone)} x,
count(*) y
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 1
`,
params,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'UTC', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select
${getDateStringQuery('g.t', unit)} as x,
g.y as y
from (
select
${getDateQuery('created_at', unit, timezone)} as t,
count(*) as y
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_type = {eventType:UInt32}
${filterQuery}
group by t
) as g
order by t
`,
params,
);
}

View file

@ -0,0 +1,208 @@
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
export async function getFunnel(
...args: [
websiteId: string,
criteria: {
windowMinutes: number;
startDate: Date;
endDate: Date;
urls: string[];
},
]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
criteria: {
windowMinutes: number;
startDate: Date;
endDate: Date;
urls: string[];
},
): Promise<
{
x: string;
y: number;
z: number;
}[]
> {
const { windowMinutes, startDate, endDate, urls } = criteria;
const { rawQuery, getAddMinutesQuery } = prisma;
const { levelQuery, sumQuery } = getFunnelQuery(urls, windowMinutes);
function getFunnelQuery(
urls: string[],
windowMinutes: number,
): {
levelQuery: string;
sumQuery: string;
} {
return urls.reduce(
(pv, cv, i) => {
const levelNumber = i + 1;
const startSum = i > 0 ? 'union ' : '';
if (levelNumber >= 2) {
pv.levelQuery += `
, level${levelNumber} AS (
select distinct we.session_id, we.created_at
from level${i} l
join website_event we
on l.session_id = we.session_id
where we.website_id = {{websiteId::uuid}}
and we.created_at between l.created_at and ${getAddMinutesQuery(
`l.created_at `,
windowMinutes,
)}
and we.referrer_path = {{${i - 1}}}
and we.url_path = {{${i}}}
and we.created_at <= {{endDate}}
)`;
}
pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`;
return pv;
},
{
levelQuery: '',
sumQuery: '',
},
);
}
return rawQuery(
`
WITH level1 AS (
select distinct session_id, created_at
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and url_path = {{0}}
)
${levelQuery}
${sumQuery}
ORDER BY level;
`,
{
websiteId,
startDate,
endDate,
...urls,
},
).then(results => {
return urls.map((a, i) => ({
x: a,
y: results[i]?.count || 0,
z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off
}));
});
}
async function clickhouseQuery(
websiteId: string,
criteria: {
windowMinutes: number;
startDate: Date;
endDate: Date;
urls: string[];
},
): Promise<
{
x: string;
y: number;
z: number;
}[]
> {
const { windowMinutes, startDate, endDate, urls } = criteria;
const { rawQuery } = clickhouse;
const { levelQuery, sumQuery, urlFilterQuery, urlParams } = getFunnelQuery(urls, windowMinutes);
function getFunnelQuery(
urls: string[],
windowMinutes: number,
): {
levelQuery: string;
sumQuery: string;
urlFilterQuery: string;
urlParams: { [key: string]: string };
} {
return urls.reduce(
(pv, cv, i) => {
const levelNumber = i + 1;
const startSum = i > 0 ? 'union all ' : '';
const startFilter = i > 0 ? ', ' : '';
if (levelNumber >= 2) {
pv.levelQuery += `\n
, level${levelNumber} AS (
select distinct y.session_id as session_id,
y.url_path as url_path,
y.referrer_path as referrer_path,
y.created_at as created_at
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
and y.referrer_path = {url${i - 1}:String}
and y.url_path = {url${i}:String}
)`;
}
pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`;
pv.urlFilterQuery += `${startFilter}{url${i}:String} `;
pv.urlParams[`url${i}`] = cv;
return pv;
},
{
levelQuery: '',
sumQuery: '',
urlFilterQuery: '',
urlParams: {},
},
);
}
return rawQuery<{ level: number; count: number }[]>(
`
WITH level0 AS (
select distinct session_id, url_path, referrer_path, created_at
from umami.website_event
where url_path in (${urlFilterQuery})
and website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
),
level1 AS (
select *
from level0
where url_path = {url0:String}
)
${levelQuery}
select *
from (
${sumQuery}
) ORDER BY level;
`,
{
websiteId,
startDate,
endDate,
...urlParams,
},
).then(results => {
return urls.map((a, i) => ({
x: a,
y: results[i]?.count || 0,
z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off
}));
});
}

View file

@ -0,0 +1,107 @@
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants';
import { QueryFilters } from 'lib/types';
export async function getInsights(
...args: [websiteId: string, fields: { name: string; type?: string }[], filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
fields: { name: string; type?: string }[],
filters: QueryFilters,
): Promise<
{
x: string;
y: number;
}[]
> {
const { parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(
websiteId,
{
...filters,
eventType: EVENT_TYPE.pageView,
},
{
joinSession: !!fields.find(({ name }) => SESSION_COLUMNS.includes(name)),
},
);
return rawQuery(
`
select
${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)}
order by 1 desc, 2 desc
limit 500
`,
params,
);
}
async function clickhouseQuery(
websiteId: string,
fields: { name: string; type?: string }[],
filters: QueryFilters,
): Promise<
{
x: string;
y: number;
}[]
> {
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select
${parseFields(fields)}
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_type = {eventType:UInt32}
${filterQuery}
${parseGroupBy(fields)}
order by 1 desc, 2 desc
limit 500
`,
params,
);
}
function parseFields(fields) {
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) {
if (!fields.length) {
return '';
}
return `group by ${fields.map(({ name }) => FILTER_COLUMNS[name]).join(',')}`;
}

View file

@ -0,0 +1,176 @@
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
export async function getRetention(
...args: [
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone: string;
},
]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone: string;
},
): Promise<
{
date: string;
day: number;
visitors: number;
returnVisitors: number;
percentage: number;
}[]
> {
const { startDate, endDate, timezone = 'UTC' } = filters;
const { getDateQuery, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
const unit = 'day';
return rawQuery(
`
WITH cohort_items AS (
select session_id,
${getDateQuery('created_at', unit, timezone)} as cohort_date
from session
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
),
user_activities AS (
select distinct
w.session_id,
${getDayDiffQuery(
getDateQuery('created_at', unit, timezone),
'c.cohort_date',
)} as day_number
from website_event w
join cohort_items c
on w.session_id = c.session_id
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
),
cohort_size as (
select cohort_date,
count(*) as visitors
from cohort_items
group by 1
order by 1
),
cohort_date as (
select
c.cohort_date,
a.day_number,
count(*) as visitors
from user_activities a
join cohort_items c
on a.session_id = c.session_id
group by 1, 2
)
select
c.cohort_date as date,
c.day_number as day,
s.visitors,
c.visitors as "returnVisitors",
${getCastColumnQuery('c.visitors', 'float')} * 100 / s.visitors as percentage
from cohort_date c
join cohort_size s
on c.cohort_date = s.cohort_date
where c.day_number <= 31
order by 1, 2`,
{
websiteId,
startDate,
endDate,
},
).then(results => {
return results.map(i => ({ ...i, percentage: Number(i.percentage) || 0 }));
});
}
async function clickhouseQuery(
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone: string;
},
): Promise<
{
date: string;
day: number;
visitors: number;
returnVisitors: number;
percentage: number;
}[]
> {
const { startDate, endDate, timezone = 'UTC' } = filters;
const { getDateQuery, getDateStringQuery, rawQuery } = clickhouse;
const unit = 'day';
return rawQuery(
`
WITH cohort_items AS (
select
min(${getDateQuery('created_at', unit, timezone)}) as cohort_date,
session_id
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
group by session_id
),
user_activities AS (
select distinct
w.session_id,
(${getDateQuery('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number
from website_event w
join cohort_items c
on w.session_id = c.session_id
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
),
cohort_size as (
select cohort_date,
count(*) as visitors
from cohort_items
group by 1
order by 1
),
cohort_date as (
select
c.cohort_date,
a.day_number,
count(*) as visitors
from user_activities a
join cohort_items c
on a.session_id = c.session_id
group by 1, 2
)
select
${getDateStringQuery('c.cohort_date', unit)} as date,
c.day_number as day,
s.visitors as visitors,
c.visitors returnVisitors,
c.visitors * 100 / s.visitors as percentage
from cohort_date c
join cohort_size s
on c.cohort_date = s.cohort_date
where c.day_number <= 31
order by 1, 2`,
{
websiteId,
startDate,
endDate,
},
);
}

View file

@ -0,0 +1,45 @@
import { Prisma } from '@prisma/client';
import cache from 'lib/cache';
import prisma from 'lib/prisma';
export async function createSession(data: Prisma.SessionCreateInput) {
const {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
} = data;
return prisma.client.session
.create({
data: {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1: country && subdivision1 ? `${country}-${subdivision1}` : null,
subdivision2,
city,
},
})
.then(async data => {
if (cache.enabled) {
await cache.storeSession(data);
}
return data;
});
}

View file

@ -0,0 +1,9 @@
import prisma from 'lib/prisma';
export async function getSession(id: string) {
return prisma.client.session.findUnique({
where: {
id,
},
});
}

View file

@ -0,0 +1,67 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants';
import { QueryFilters } from 'lib/types';
export async function getSessionMetrics(
...args: [websiteId: string, column: string, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) {
const { parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(
websiteId,
{
...filters,
eventType: EVENT_TYPE.pageView,
},
{
joinSession: SESSION_COLUMNS.includes(column),
},
);
return rawQuery(
`
select ${column} x, count(*) y
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}
group by 1
order by 2 desc
limit 100`,
params,
);
}
async function clickhouseQuery(websiteId: string, column: string, filters: QueryFilters) {
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select
${column} x, count(distinct session_id) y
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_type = {eventType:UInt32}
${filterQuery}
group by x
order by y desc
limit 100
`,
params,
);
}

View file

@ -0,0 +1,67 @@
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
import { EVENT_TYPE } from 'lib/constants';
import { QueryFilters } from 'lib/types';
export async function getSessionStats(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { getDateQuery, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select
${getDateQuery('website_event.created_at', unit, timezone)} x,
count(distinct website_event.session_id) y
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 1
`,
params,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'UTC', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select
${getDateStringQuery('g.t', unit)} as x,
g.y as y
from (
select
${getDateQuery('created_at', unit, timezone)} as t,
count(distinct session_id) as y
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_type = {eventType:UInt32}
${filterQuery}
group by t
) as g
order by t
`,
params,
);
}

View file

@ -0,0 +1,52 @@
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db';
export async function getSessions(...args: [websiteId: string, startAt: Date]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, startDate: Date) {
return prisma.client.session.findMany({
where: {
websiteId,
createdAt: {
gte: startDate,
},
},
});
}
async function clickhouseQuery(websiteId: string, startDate: Date) {
const { rawQuery } = clickhouse;
return rawQuery(
`
select distinct
session_id as id,
website_id as websiteId,
created_at as createdAt,
toUnixTimestamp(created_at) as timestamp,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city
from website_event
where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime}
`,
{
websiteId,
startDate,
},
);
}

View file

@ -0,0 +1,43 @@
import { DATA_TYPE } from 'lib/constants';
import { uuid } from 'lib/crypto';
import { flattenJSON } from 'lib/data';
import prisma from 'lib/prisma';
import { DynamicData } from 'lib/types';
export async function saveSessionData(data: {
websiteId: string;
sessionId: string;
sessionData: DynamicData;
}) {
const { client, transaction } = prisma;
const { websiteId, sessionId, sessionData } = data;
const jsonKeys = flattenJSON(sessionData);
const flattendData = jsonKeys.map(a => ({
id: uuid(),
websiteId,
sessionId,
key: a.key,
stringValue:
a.dynamicDataType === DATA_TYPE.number
? parseFloat(a.value).toFixed(4)
: a.dynamicDataType === DATA_TYPE.date
? a.value.split('.')[0] + 'Z'
: a.value.toString(),
numberValue: a.dynamicDataType === DATA_TYPE.number ? a.value : null,
dateValue: a.dynamicDataType === DATA_TYPE.date ? new Date(a.value) : null,
dataType: a.dynamicDataType,
}));
return transaction([
client.sessionData.deleteMany({
where: {
sessionId,
},
}),
client.sessionData.createMany({
data: flattendData as any,
}),
]);
}