mirror of
https://github.com/umami-software/umami.git
synced 2026-02-06 13:47:15 +01:00
Moved code into src folder. Added build for component library.
This commit is contained in:
parent
7a7233ead4
commit
ede658771e
490 changed files with 749 additions and 442 deletions
180
src/queries/admin/report.ts
Normal file
180
src/queries/admin/report.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { Prisma, Report } from '@prisma/client';
|
||||
import { REPORT_FILTER_TYPES } from 'lib/constants';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, ReportSearchFilter } from 'lib/types';
|
||||
|
||||
export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise<Report> {
|
||||
return prisma.client.report.create({ data });
|
||||
}
|
||||
|
||||
export async function getReportById(reportId: string): Promise<Report> {
|
||||
return prisma.client.report.findUnique({
|
||||
where: {
|
||||
id: reportId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateReport(
|
||||
reportId: string,
|
||||
data: Prisma.ReportUpdateInput,
|
||||
): Promise<Report> {
|
||||
return prisma.client.report.update({ where: { id: reportId }, data });
|
||||
}
|
||||
|
||||
export async function deleteReport(reportId: string): Promise<Report> {
|
||||
return prisma.client.report.delete({ where: { id: reportId } });
|
||||
}
|
||||
|
||||
export async function getReports(
|
||||
ReportSearchFilter: ReportSearchFilter,
|
||||
options?: { include?: Prisma.ReportInclude },
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
const {
|
||||
userId,
|
||||
websiteId,
|
||||
includeTeams,
|
||||
filter,
|
||||
filterType = REPORT_FILTER_TYPES.all,
|
||||
} = ReportSearchFilter;
|
||||
|
||||
const mode = prisma.getSearchMode();
|
||||
|
||||
const where: Prisma.ReportWhereInput = {
|
||||
...(userId && { userId: userId }),
|
||||
...(websiteId && { websiteId: websiteId }),
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
...(userId && { userId: userId }),
|
||||
},
|
||||
{
|
||||
...(includeTeams && {
|
||||
website: {
|
||||
teamWebsite: {
|
||||
some: {
|
||||
team: {
|
||||
teamUser: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES.name) && {
|
||||
name: {
|
||||
startsWith: filter,
|
||||
...mode,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES.description) && {
|
||||
description: {
|
||||
startsWith: filter,
|
||||
...mode,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES.type) && {
|
||||
type: {
|
||||
startsWith: filter,
|
||||
...mode,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES['user:username']) && {
|
||||
user: {
|
||||
username: {
|
||||
startsWith: filter,
|
||||
...mode,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES['website:name']) && {
|
||||
website: {
|
||||
name: {
|
||||
startsWith: filter,
|
||||
...mode,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES['website:domain']) && {
|
||||
website: {
|
||||
domain: {
|
||||
startsWith: filter,
|
||||
...mode,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter);
|
||||
|
||||
const reports = await prisma.client.report.findMany({
|
||||
where,
|
||||
...pageFilters,
|
||||
...(options?.include && { include: options.include }),
|
||||
});
|
||||
const count = await prisma.client.report.count({
|
||||
where,
|
||||
});
|
||||
|
||||
return {
|
||||
data: reports,
|
||||
count,
|
||||
...getParameters,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getReportsByUserId(
|
||||
userId: string,
|
||||
filter: ReportSearchFilter,
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
return getReports(
|
||||
{ userId, ...filter },
|
||||
{
|
||||
include: {
|
||||
website: {
|
||||
select: {
|
||||
domain: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function getReportsByWebsiteId(
|
||||
websiteId: string,
|
||||
filter: ReportSearchFilter,
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
return getReports({ websiteId, ...filter });
|
||||
}
|
||||
164
src/queries/admin/team.ts
Normal file
164
src/queries/admin/team.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { Prisma, Team } from '@prisma/client';
|
||||
import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, TeamSearchFilter } from 'lib/types';
|
||||
|
||||
export interface GetTeamOptions {
|
||||
includeTeamUser?: boolean;
|
||||
}
|
||||
|
||||
async function getTeam(where: Prisma.TeamWhereInput, options: GetTeamOptions = {}): Promise<Team> {
|
||||
const { includeTeamUser = false } = options;
|
||||
|
||||
return prisma.client.team.findFirst({
|
||||
where,
|
||||
include: {
|
||||
teamUser: includeTeamUser,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getTeamById(teamId: string, options: GetTeamOptions = {}) {
|
||||
return getTeam({ id: teamId }, options);
|
||||
}
|
||||
|
||||
export function getTeamByAccessCode(accessCode: string, options: GetTeamOptions = {}) {
|
||||
return getTeam({ accessCode }, options);
|
||||
}
|
||||
|
||||
export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<Team> {
|
||||
const { id } = data;
|
||||
|
||||
return prisma.transaction([
|
||||
prisma.client.team.create({
|
||||
data,
|
||||
}),
|
||||
prisma.client.teamUser.create({
|
||||
data: {
|
||||
id: uuid(),
|
||||
teamId: id,
|
||||
userId,
|
||||
role: ROLES.teamOwner,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function updateTeam(teamId: string, data: Prisma.TeamUpdateInput): Promise<Team> {
|
||||
return prisma.client.team.update({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
data: {
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteTeam(
|
||||
teamId: string,
|
||||
): Promise<Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Team]>> {
|
||||
const { client, transaction } = prisma;
|
||||
|
||||
return transaction([
|
||||
client.teamWebsite.deleteMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
}),
|
||||
client.teamUser.deleteMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
}),
|
||||
client.team.delete({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function getTeams(
|
||||
TeamSearchFilter: TeamSearchFilter,
|
||||
options?: { include?: Prisma.TeamInclude },
|
||||
): Promise<FilterResult<Team[]>> {
|
||||
const { userId, filter, filterType = TEAM_FILTER_TYPES.all } = TeamSearchFilter;
|
||||
const mode = prisma.getSearchMode();
|
||||
|
||||
const where: Prisma.TeamWhereInput = {
|
||||
...(userId && {
|
||||
teamUser: {
|
||||
some: { userId },
|
||||
},
|
||||
}),
|
||||
...(filter && {
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && {
|
||||
name: { startsWith: filter, ...mode },
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === TEAM_FILTER_TYPES.all ||
|
||||
filterType === TEAM_FILTER_TYPES['user:username']) && {
|
||||
teamUser: {
|
||||
some: {
|
||||
role: ROLES.teamOwner,
|
||||
user: {
|
||||
username: {
|
||||
startsWith: filter,
|
||||
...mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const [pageFilters, getParameters] = prisma.getPageFilters({
|
||||
orderBy: 'name',
|
||||
...TeamSearchFilter,
|
||||
});
|
||||
|
||||
const teams = await prisma.client.team.findMany({
|
||||
where: {
|
||||
...where,
|
||||
},
|
||||
...pageFilters,
|
||||
...(options?.include && { include: options?.include }),
|
||||
});
|
||||
const count = await prisma.client.team.count({ where });
|
||||
|
||||
return { data: teams, count, ...getParameters };
|
||||
}
|
||||
|
||||
export async function getTeamsByUserId(
|
||||
userId: string,
|
||||
filter?: TeamSearchFilter,
|
||||
): Promise<FilterResult<Team[]>> {
|
||||
return getTeams(
|
||||
{ userId, ...filter },
|
||||
{
|
||||
include: {
|
||||
teamUser: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
98
src/queries/admin/teamUser.ts
Normal file
98
src/queries/admin/teamUser.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Prisma, TeamUser } from '@prisma/client';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getTeamUserById(teamUserId: string): Promise<TeamUser> {
|
||||
return prisma.client.teamUser.findUnique({
|
||||
where: {
|
||||
id: teamUserId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTeamUser(teamId: string, userId: string): Promise<TeamUser> {
|
||||
return prisma.client.teamUser.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTeamUsers(
|
||||
teamId: string,
|
||||
): Promise<(TeamUser & { user: { id: string; username: string } })[]> {
|
||||
return prisma.client.teamUser.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTeamUser(
|
||||
userId: string,
|
||||
teamId: string,
|
||||
role: string,
|
||||
): Promise<TeamUser> {
|
||||
return prisma.client.teamUser.create({
|
||||
data: {
|
||||
id: uuid(),
|
||||
userId,
|
||||
teamId,
|
||||
role,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateTeamUser(
|
||||
teamUserId: string,
|
||||
data: Prisma.TeamUserUpdateInput,
|
||||
): Promise<TeamUser> {
|
||||
return prisma.client.teamUser.update({
|
||||
where: {
|
||||
id: teamUserId,
|
||||
},
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteTeamUser(teamId: string, userId: string): Promise<TeamUser> {
|
||||
const { client, transaction } = prisma;
|
||||
|
||||
return transaction([
|
||||
client.teamWebsite.deleteMany({
|
||||
where: {
|
||||
teamId: teamId,
|
||||
website: {
|
||||
userId: userId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
client.teamUser.deleteMany({
|
||||
where: {
|
||||
teamId,
|
||||
userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function deleteTeamUserByUserId(
|
||||
userId: string,
|
||||
teamId: string,
|
||||
): Promise<Prisma.BatchPayload> {
|
||||
return prisma.client.teamUser.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
127
src/queries/admin/teamWebsite.ts
Normal file
127
src/queries/admin/teamWebsite.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { Prisma, Team, TeamUser, TeamWebsite, Website } from '@prisma/client';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getTeamWebsite(
|
||||
teamId: string,
|
||||
websiteId: string,
|
||||
): Promise<
|
||||
TeamWebsite & {
|
||||
website: Website;
|
||||
}
|
||||
> {
|
||||
return prisma.client.teamWebsite.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
websiteId,
|
||||
},
|
||||
include: {
|
||||
website: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function findTeamWebsiteByUserId(
|
||||
websiteId: string,
|
||||
userId: string,
|
||||
): Promise<TeamWebsite> {
|
||||
return prisma.client.teamWebsite.findFirst({
|
||||
where: {
|
||||
websiteId,
|
||||
team: {
|
||||
teamUser: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTeamWebsites(teamId: string): Promise<
|
||||
(TeamWebsite & {
|
||||
team: Team & { teamUser: TeamUser[] };
|
||||
website: Website & {
|
||||
user: { id: string; username: string };
|
||||
};
|
||||
})[]
|
||||
> {
|
||||
return prisma.client.teamWebsite.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
teamUser: {
|
||||
where: {
|
||||
role: ROLES.teamOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
website: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
team: {
|
||||
name: 'asc',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTeamWebsite(teamId: string, websiteId: string): Promise<TeamWebsite> {
|
||||
return prisma.client.teamWebsite.create({
|
||||
data: {
|
||||
id: uuid(),
|
||||
teamId,
|
||||
websiteId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTeamWebsites(teamId: string, websiteIds: string[]) {
|
||||
const currentTeamWebsites = await getTeamWebsites(teamId);
|
||||
|
||||
// filter out websites that already exists on the team
|
||||
const addWebsites = websiteIds.filter(
|
||||
websiteId => !currentTeamWebsites.some(a => a.websiteId === websiteId),
|
||||
);
|
||||
|
||||
const teamWebsites: Prisma.TeamWebsiteCreateManyInput[] = addWebsites.map(a => {
|
||||
return {
|
||||
id: uuid(),
|
||||
teamId,
|
||||
websiteId: a,
|
||||
};
|
||||
});
|
||||
|
||||
return prisma.client.teamWebsite.createMany({
|
||||
data: teamWebsites,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteTeamWebsite(
|
||||
teamId: string,
|
||||
websiteId: string,
|
||||
): Promise<Prisma.BatchPayload> {
|
||||
return prisma.client.teamWebsite.deleteMany({
|
||||
where: {
|
||||
teamId,
|
||||
websiteId,
|
||||
},
|
||||
});
|
||||
}
|
||||
284
src/queries/admin/user.ts
Normal file
284
src/queries/admin/user.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import { Prisma } from '@prisma/client';
|
||||
import cache from 'lib/cache';
|
||||
import { ROLES, USER_FILTER_TYPES } from 'lib/constants';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
|
||||
import { getRandomChars } from 'next-basics';
|
||||
|
||||
export interface GetUserOptions {
|
||||
includePassword?: boolean;
|
||||
showDeleted?: boolean;
|
||||
}
|
||||
|
||||
async function getUser(
|
||||
where: Prisma.UserWhereInput | Prisma.UserWhereUniqueInput,
|
||||
options: GetUserOptions = {},
|
||||
): Promise<User> {
|
||||
const { includePassword = false, showDeleted = false } = options;
|
||||
|
||||
return prisma.client.user.findFirst({
|
||||
where: { ...where, ...(showDeleted ? {} : { deletedAt: null }) },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
password: includePassword,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserById(userId: string, options: GetUserOptions = {}) {
|
||||
return getUser({ id: userId }, options);
|
||||
}
|
||||
|
||||
export async function getUserByUsername(username: string, options: GetUserOptions = {}) {
|
||||
return getUser({ username }, options);
|
||||
}
|
||||
|
||||
export async function getUsers(
|
||||
searchFilter: UserSearchFilter,
|
||||
options?: { include?: Prisma.UserInclude },
|
||||
): Promise<FilterResult<User[]>> {
|
||||
const { teamId, filter, filterType = USER_FILTER_TYPES.all } = searchFilter;
|
||||
const mode = prisma.getSearchMode();
|
||||
|
||||
const where: Prisma.UserWhereInput = {
|
||||
...(teamId && {
|
||||
teamUser: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(filter && {
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
...((filterType === USER_FILTER_TYPES.all ||
|
||||
filterType === USER_FILTER_TYPES.username) && {
|
||||
username: {
|
||||
startsWith: filter,
|
||||
...mode,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const [pageFilters, getParameters] = prisma.getPageFilters({
|
||||
orderBy: 'username',
|
||||
...searchFilter,
|
||||
});
|
||||
|
||||
const users = await prisma.client.user
|
||||
.findMany({
|
||||
where: {
|
||||
...where,
|
||||
deletedAt: null,
|
||||
},
|
||||
...pageFilters,
|
||||
...(options?.include && { include: options.include }),
|
||||
})
|
||||
.then(a => {
|
||||
return a.map(({ password, ...rest }) => rest);
|
||||
});
|
||||
|
||||
const count = await prisma.client.user.count({
|
||||
where: {
|
||||
...where,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
return { data: users as any, count, ...getParameters };
|
||||
}
|
||||
|
||||
export async function getUsersByTeamId(teamId: string, filter?: UserSearchFilter) {
|
||||
return getUsers({ teamId, ...filter });
|
||||
}
|
||||
|
||||
export async function createUser(data: {
|
||||
id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
role: Role;
|
||||
}): Promise<{
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
}> {
|
||||
return prisma.client.user.create({
|
||||
data,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateUser(
|
||||
data: Prisma.UserUpdateInput,
|
||||
where: Prisma.UserWhereUniqueInput,
|
||||
): Promise<User> {
|
||||
return prisma.client.user.update({
|
||||
where,
|
||||
data,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteUser(
|
||||
userId: string,
|
||||
): Promise<
|
||||
[
|
||||
Prisma.BatchPayload,
|
||||
Prisma.BatchPayload,
|
||||
Prisma.BatchPayload,
|
||||
Prisma.BatchPayload,
|
||||
Prisma.BatchPayload,
|
||||
Prisma.BatchPayload,
|
||||
User,
|
||||
]
|
||||
> {
|
||||
const { client } = prisma;
|
||||
const cloudMode = process.env.CLOUD_MODE;
|
||||
|
||||
const websites = await client.website.findMany({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
let websiteIds = [];
|
||||
|
||||
if (websites.length > 0) {
|
||||
websiteIds = websites.map(a => a.id);
|
||||
}
|
||||
|
||||
const teams = await client.team.findMany({
|
||||
where: {
|
||||
teamUser: {
|
||||
some: {
|
||||
userId,
|
||||
role: ROLES.teamOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const teamIds = teams.map(a => a.id);
|
||||
|
||||
return prisma
|
||||
.transaction([
|
||||
client.eventData.deleteMany({
|
||||
where: { websiteId: { in: websiteIds } },
|
||||
}),
|
||||
client.websiteEvent.deleteMany({
|
||||
where: { websiteId: { in: websiteIds } },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { websiteId: { in: websiteIds } },
|
||||
}),
|
||||
client.teamWebsite.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
websiteId: {
|
||||
in: websiteIds,
|
||||
},
|
||||
},
|
||||
{
|
||||
teamId: {
|
||||
in: teamIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
client.teamWebsite.deleteMany({
|
||||
where: {
|
||||
teamId: {
|
||||
in: teamIds,
|
||||
},
|
||||
},
|
||||
}),
|
||||
client.teamUser.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
teamId: {
|
||||
in: teamIds,
|
||||
},
|
||||
},
|
||||
{
|
||||
userId,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
client.team.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: teamIds,
|
||||
},
|
||||
},
|
||||
}),
|
||||
client.report.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
websiteId: {
|
||||
in: websiteIds,
|
||||
},
|
||||
},
|
||||
{
|
||||
userId,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
cloudMode
|
||||
? client.website.updateMany({
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
where: { id: { in: websiteIds } },
|
||||
})
|
||||
: client.website.deleteMany({
|
||||
where: { id: { in: websiteIds } },
|
||||
}),
|
||||
cloudMode
|
||||
? client.user.update({
|
||||
data: {
|
||||
username: getRandomChars(32),
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
: client.user.delete({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
])
|
||||
.then(async data => {
|
||||
if (cache.enabled) {
|
||||
const ids = websites.map(a => a.id);
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
await cache.deleteWebsite(`website:${ids[i]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
349
src/queries/admin/website.ts
Normal file
349
src/queries/admin/website.ts
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
import { Prisma, Website } from '@prisma/client';
|
||||
import cache from 'lib/cache';
|
||||
import { ROLES, WEBSITE_FILTER_TYPES } from 'lib/constants';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, WebsiteSearchFilter } from 'lib/types';
|
||||
|
||||
async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise<Website> {
|
||||
return prisma.client.website.findUnique({
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWebsiteById(id: string) {
|
||||
return getWebsite({ id });
|
||||
}
|
||||
|
||||
export async function getWebsiteByShareId(shareId: string) {
|
||||
return getWebsite({ shareId });
|
||||
}
|
||||
|
||||
export async function getWebsites(
|
||||
WebsiteSearchFilter: WebsiteSearchFilter,
|
||||
options?: { include?: Prisma.WebsiteInclude },
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
const {
|
||||
userId,
|
||||
teamId,
|
||||
includeTeams,
|
||||
onlyTeams,
|
||||
filter,
|
||||
filterType = WEBSITE_FILTER_TYPES.all,
|
||||
} = WebsiteSearchFilter;
|
||||
const mode = prisma.getSearchMode();
|
||||
|
||||
const where: Prisma.WebsiteWhereInput = {
|
||||
...(teamId && {
|
||||
teamWebsite: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
...(userId &&
|
||||
!onlyTeams && {
|
||||
userId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((includeTeams || onlyTeams) && {
|
||||
AND: [
|
||||
{
|
||||
teamWebsite: {
|
||||
some: {
|
||||
team: {
|
||||
teamUser: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: {
|
||||
not: userId,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
...((filterType === WEBSITE_FILTER_TYPES.all ||
|
||||
filterType === WEBSITE_FILTER_TYPES.name) && {
|
||||
name: { startsWith: filter, ...mode },
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === WEBSITE_FILTER_TYPES.all ||
|
||||
filterType === WEBSITE_FILTER_TYPES.domain) && {
|
||||
domain: { startsWith: filter, ...mode },
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const [pageFilters, getParameters] = prisma.getPageFilters({
|
||||
orderBy: 'name',
|
||||
...WebsiteSearchFilter,
|
||||
});
|
||||
|
||||
const websites = await prisma.client.website.findMany({
|
||||
where: {
|
||||
...where,
|
||||
deletedAt: null,
|
||||
},
|
||||
...pageFilters,
|
||||
...(options?.include && { include: options.include }),
|
||||
});
|
||||
const count = await prisma.client.website.count({ where });
|
||||
|
||||
return { data: websites, count, ...getParameters };
|
||||
}
|
||||
|
||||
export async function getWebsitesByUserId(
|
||||
userId: string,
|
||||
filter?: WebsiteSearchFilter,
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
return getWebsites(
|
||||
{ userId, ...filter },
|
||||
{
|
||||
include: {
|
||||
teamWebsite: {
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function getWebsitesByTeamId(
|
||||
teamId: string,
|
||||
filter?: WebsiteSearchFilter,
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
return getWebsites(
|
||||
{
|
||||
teamId,
|
||||
...filter,
|
||||
includeTeams: true,
|
||||
},
|
||||
{
|
||||
include: {
|
||||
teamWebsite: {
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
teamUser: {
|
||||
where: { role: ROLES.teamOwner },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUserWebsites(
|
||||
userId: string,
|
||||
options?: { includeTeams: boolean },
|
||||
): Promise<Website[]> {
|
||||
const { rawQuery } = prisma;
|
||||
|
||||
if (options?.includeTeams) {
|
||||
const websites = await rawQuery(
|
||||
`
|
||||
select
|
||||
website_id as "id",
|
||||
name,
|
||||
domain,
|
||||
share_id as "shareId",
|
||||
reset_at as "resetAt",
|
||||
user_id as "userId",
|
||||
created_at as "createdAt",
|
||||
updated_at as "updatedAt",
|
||||
deleted_at as "deletedAt",
|
||||
null as "teamId",
|
||||
null as "teamName"
|
||||
from website
|
||||
where user_id = {{userId::uuid}}
|
||||
and deleted_at is null
|
||||
union
|
||||
select
|
||||
w.website_id as "id",
|
||||
w.name,
|
||||
w.domain,
|
||||
w.share_id as "shareId",
|
||||
w.reset_at as "resetAt",
|
||||
w.user_id as "userId",
|
||||
w.created_at as "createdAt",
|
||||
w.updated_at as "updatedAt",
|
||||
w.deleted_at as "deletedAt",
|
||||
t.team_id as "teamId",
|
||||
t.name as "teamName"
|
||||
from website w
|
||||
inner join team_website tw
|
||||
on tw.website_id = w.website_id
|
||||
inner join team t
|
||||
on t.team_id = tw.team_id
|
||||
inner join team_user tu
|
||||
on tu.team_id = tw.team_id
|
||||
where tu.user_id = {{userId::uuid}}
|
||||
and w.deleted_at is null
|
||||
`,
|
||||
{ userId },
|
||||
);
|
||||
|
||||
return websites.reduce((arr, item) => {
|
||||
if (!arr.find(({ id }) => id === item.id)) {
|
||||
return arr.concat(item);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return prisma.client.website.findMany({
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
name: 'asc',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export async function createWebsite(
|
||||
data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput,
|
||||
): Promise<Website> {
|
||||
return prisma.client.website
|
||||
.create({
|
||||
data,
|
||||
})
|
||||
.then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.storeWebsite(data);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateWebsite(
|
||||
websiteId,
|
||||
data: Prisma.WebsiteUpdateInput | Prisma.WebsiteUncheckedUpdateInput,
|
||||
): Promise<Website> {
|
||||
return prisma.client.website.update({
|
||||
where: {
|
||||
id: websiteId,
|
||||
},
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function resetWebsite(
|
||||
websiteId,
|
||||
): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> {
|
||||
const { client, transaction } = prisma;
|
||||
|
||||
return transaction([
|
||||
client.eventData.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.websiteEvent.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.website.update({
|
||||
where: { id: websiteId },
|
||||
data: {
|
||||
resetAt: new Date(),
|
||||
},
|
||||
}),
|
||||
]).then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.storeWebsite(data[2]);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteWebsite(
|
||||
websiteId,
|
||||
): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> {
|
||||
const { client, transaction } = prisma;
|
||||
const cloudMode = process.env.CLOUD_MODE;
|
||||
|
||||
return transaction([
|
||||
client.eventData.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.websiteEvent.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.teamWebsite.deleteMany({
|
||||
where: {
|
||||
websiteId,
|
||||
},
|
||||
}),
|
||||
client.report.deleteMany({
|
||||
where: {
|
||||
websiteId,
|
||||
},
|
||||
}),
|
||||
cloudMode
|
||||
? prisma.client.website.update({
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
where: { id: websiteId },
|
||||
})
|
||||
: client.website.delete({
|
||||
where: { id: websiteId },
|
||||
}),
|
||||
]).then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.deleteWebsite(websiteId);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
104
src/queries/analytics/eventData/getEventDataEvents.ts
Normal file
104
src/queries/analytics/eventData/getEventDataEvents.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
63
src/queries/analytics/eventData/getEventDataFields.ts
Normal file
63
src/queries/analytics/eventData/getEventDataFields.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
69
src/queries/analytics/eventData/getEventDataStats.ts
Normal file
69
src/queries/analytics/eventData/getEventDataStats.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
30
src/queries/analytics/eventData/getEventDataUsage.ts
Normal file
30
src/queries/analytics/eventData/getEventDataUsage.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
91
src/queries/analytics/eventData/saveEventData.ts
Normal file
91
src/queries/analytics/eventData/saveEventData.ts
Normal 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;
|
||||
}
|
||||
67
src/queries/analytics/events/getEventMetrics.ts
Normal file
67
src/queries/analytics/events/getEventMetrics.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
30
src/queries/analytics/events/getEventUsage.ts
Normal file
30
src/queries/analytics/events/getEventUsage.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
49
src/queries/analytics/events/getEvents.ts
Normal file
49
src/queries/analytics/events/getEvents.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
170
src/queries/analytics/events/saveEvent.ts
Normal file
170
src/queries/analytics/events/saveEvent.ts
Normal 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;
|
||||
}
|
||||
40
src/queries/analytics/getActiveVisitors.ts
Normal file
40
src/queries/analytics/getActiveVisitors.ts
Normal 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) },
|
||||
);
|
||||
}
|
||||
27
src/queries/analytics/getRealtimeData.ts
Normal file
27
src/queries/analytics/getRealtimeData.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
38
src/queries/analytics/getValues.ts
Normal file
38
src/queries/analytics/getValues.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
49
src/queries/analytics/getWebsiteDateRange.ts
Normal file
49
src/queries/analytics/getWebsiteDateRange.ts
Normal 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;
|
||||
}
|
||||
80
src/queries/analytics/getWebsiteStats.ts
Normal file
80
src/queries/analytics/getWebsiteStats.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
65
src/queries/analytics/pageviews/getPageviewMetrics.ts
Normal file
65
src/queries/analytics/pageviews/getPageviewMetrics.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
67
src/queries/analytics/pageviews/getPageviewStats.ts
Normal file
67
src/queries/analytics/pageviews/getPageviewStats.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
208
src/queries/analytics/reports/getFunnel.ts
Normal file
208
src/queries/analytics/reports/getFunnel.ts
Normal 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
|
||||
}));
|
||||
});
|
||||
}
|
||||
107
src/queries/analytics/reports/getInsights.ts
Normal file
107
src/queries/analytics/reports/getInsights.ts
Normal 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(',')}`;
|
||||
}
|
||||
176
src/queries/analytics/reports/getRetention.ts
Normal file
176
src/queries/analytics/reports/getRetention.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
45
src/queries/analytics/sessions/createSession.ts
Normal file
45
src/queries/analytics/sessions/createSession.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
9
src/queries/analytics/sessions/getSession.ts
Normal file
9
src/queries/analytics/sessions/getSession.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getSession(id: string) {
|
||||
return prisma.client.session.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
67
src/queries/analytics/sessions/getSessionMetrics.ts
Normal file
67
src/queries/analytics/sessions/getSessionMetrics.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
67
src/queries/analytics/sessions/getSessionStats.ts
Normal file
67
src/queries/analytics/sessions/getSessionStats.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
52
src/queries/analytics/sessions/getSessions.ts
Normal file
52
src/queries/analytics/sessions/getSessions.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
43
src/queries/analytics/sessions/saveSessionData.ts
Normal file
43
src/queries/analytics/sessions/saveSessionData.ts
Normal 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,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
30
src/queries/index.js
Normal file
30
src/queries/index.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export * from './admin/report';
|
||||
export * from './admin/team';
|
||||
export * from './admin/teamUser';
|
||||
export * from './admin/teamWebsite';
|
||||
export * from './admin/user';
|
||||
export * from './admin/website';
|
||||
export * from './analytics/events/getEventMetrics';
|
||||
export * from './analytics/events/getEventUsage';
|
||||
export * from './analytics/events/getEvents';
|
||||
export * from './analytics/eventData/getEventDataEvents';
|
||||
export * from './analytics/eventData/getEventDataFields';
|
||||
export * from './analytics/eventData/getEventDataStats';
|
||||
export * from './analytics/eventData/getEventDataUsage';
|
||||
export * from './analytics/events/saveEvent';
|
||||
export * from './analytics/reports/getFunnel';
|
||||
export * from './analytics/reports/getRetention';
|
||||
export * from './analytics/reports/getInsights';
|
||||
export * from './analytics/pageviews/getPageviewMetrics';
|
||||
export * from './analytics/pageviews/getPageviewStats';
|
||||
export * from './analytics/sessions/createSession';
|
||||
export * from './analytics/sessions/getSession';
|
||||
export * from './analytics/sessions/getSessionMetrics';
|
||||
export * from './analytics/sessions/getSessions';
|
||||
export * from './analytics/sessions/getSessionStats';
|
||||
export * from './analytics/sessions/saveSessionData';
|
||||
export * from './analytics/getActiveVisitors';
|
||||
export * from './analytics/getRealtimeData';
|
||||
export * from './analytics/getValues';
|
||||
export * from './analytics/getWebsiteDateRange';
|
||||
export * from './analytics/getWebsiteStats';
|
||||
Loading…
Add table
Add a link
Reference in a new issue