tianji/src/server/trpc/routers/website.ts

520 lines
11 KiB
TypeScript
Raw Normal View History

import {
OpenApiMetaInfo,
router,
workspaceOwnerProcedure,
workspaceProcedure,
} from '../trpc';
2023-09-28 10:01:04 +00:00
import { z } from 'zod';
import {
getWebsiteOnlineUserCount,
getWorkspaceWebsitePageview,
2024-02-17 16:56:35 +00:00
getWorkspaceWebsiteSession,
getWorkspaceWebsiteStats,
} from '../../model/website';
2023-10-06 07:04:55 +00:00
import { prisma } from '../../model/_client';
2023-10-06 14:08:15 +00:00
import {
EVENT_COLUMNS,
FILTER_COLUMNS,
OPENAPI_TAG,
2023-10-06 14:08:15 +00:00
SESSION_COLUMNS,
} from '../../utils/const';
import { parseDateRange } from '../../utils/common';
2024-02-27 12:36:56 +00:00
import {
getWebsiteSessionMetrics,
getWebsitePageviewMetrics,
} from '../../model/website';
import { websiteInfoSchema } from '../../model/_schema';
import { OpenApiMeta } from 'trpc-openapi';
2024-01-24 13:26:42 +00:00
import { hostnameRegex } from '@tianji/shared';
import {
websiteFilterSchema,
websiteStatsSchema,
} from '../../model/_schema/filter';
import dayjs from 'dayjs';
2024-02-27 12:36:56 +00:00
import { WebsiteQueryFilters } from '../../utils/prisma';
const websiteNameSchema = z.string().max(100);
const websiteDomainSchema = z.union([
z.string().max(500).regex(hostnameRegex),
z.string().max(500).ip(),
]);
2023-09-28 10:01:04 +00:00
export const websiteRouter = router({
onlineCount: workspaceProcedure
.meta(
buildWebsiteOpenapi({
method: 'GET',
path: '/onlineCount',
})
)
2023-09-28 10:01:04 +00:00
.input(
z.object({
websiteId: z.string(),
})
)
.output(z.number())
2023-09-28 10:01:04 +00:00
.query(async ({ input }) => {
const websiteId = input.websiteId;
const count = await getWebsiteOnlineUserCount(websiteId);
return count;
}),
all: workspaceProcedure
.meta({
openapi: {
method: 'GET',
2024-03-22 16:37:30 +00:00
path: '/workspace/{workspaceId}/website/all',
tags: [OPENAPI_TAG.WEBSITE],
protect: true,
},
})
.output(z.array(websiteInfoSchema))
.query(async ({ input }) => {
const { workspaceId } = input;
const websites = await prisma.website.findMany({
where: {
workspaceId,
},
orderBy: {
updatedAt: 'desc',
},
});
return websites;
}),
2023-10-06 07:04:55 +00:00
info: workspaceProcedure
.meta(
buildWebsiteOpenapi({
method: 'GET',
path: '/info',
})
)
2023-10-06 07:04:55 +00:00
.input(
z.object({
websiteId: z.string(),
})
)
.output(websiteInfoSchema.nullable())
2023-10-06 07:04:55 +00:00
.query(async ({ input }) => {
const { workspaceId, websiteId } = input;
2023-10-06 07:04:55 +00:00
const website = await prisma.website.findUnique({
where: {
id: websiteId,
workspaceId,
2023-10-06 07:04:55 +00:00
},
});
return website;
}),
stats: workspaceProcedure
.meta(
buildWebsiteOpenapi({
method: 'GET',
path: '/stats',
})
)
.input(
z
.object({
websiteId: z.string(),
startAt: z.number(),
endAt: z.number(),
unit: z.string().optional(),
})
.merge(websiteFilterSchema.partial())
)
.output(websiteStatsSchema)
.query(async ({ input }) => {
const {
websiteId,
timezone,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
startAt,
endAt,
} = input;
const { startDate, endDate, unit } = await parseDateRange({
websiteId,
startAt: Number(startAt),
endAt: Number(endAt),
unit: input.unit,
});
const diff = dayjs(endDate).diff(startDate, 'minutes');
const prevStartDate = dayjs(startDate).subtract(diff, 'minutes').toDate();
const prevEndDate = dayjs(endDate).subtract(diff, 'minutes').toDate();
const filters = {
startDate,
endDate,
timezone,
unit,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
2024-02-27 12:36:56 +00:00
} as WebsiteQueryFilters;
const [metrics, prevPeriod] = await Promise.all([
getWorkspaceWebsiteStats(websiteId, {
...filters,
startDate,
endDate,
}),
getWorkspaceWebsiteStats(websiteId, {
...filters,
startDate: prevStartDate,
endDate: prevEndDate,
}),
]);
2024-05-07 16:26:04 +00:00
const stats = Object.keys(metrics[0]).reduce(
(obj, key) => {
const current = Number(metrics[0][key]) || 0;
const prev = Number(prevPeriod[0][key]) || 0;
obj[key] = {
value: current,
prev,
};
return obj;
},
{} as Record<string, { value: number; prev: number }>
);
return websiteStatsSchema.parse(stats);
}),
2024-01-29 16:28:37 +00:00
geoStats: workspaceProcedure
.meta(
buildWebsiteOpenapi({
method: 'GET',
path: '/geoStats',
})
)
.input(
z.object({
websiteId: z.string(),
startAt: z.number(),
endAt: z.number(),
})
)
.output(
z.array(
z.object({
longitude: z.number(),
latitude: z.number(),
count: z.number(),
})
)
)
.query(async ({ input }) => {
const { websiteId, startAt, endAt } = input;
const res = await prisma.websiteSession.groupBy({
by: ['longitude', 'latitude'],
where: {
websiteId,
longitude: { not: null },
latitude: { not: null },
createdAt: {
gt: new Date(startAt),
lte: new Date(endAt),
},
},
_count: {
_all: true,
},
});
return res
.filter((item) => item.longitude !== null && item.latitude !== null)
.map((item) => {
return {
longitude: item.longitude!,
latitude: item.latitude!,
count: item._count._all,
};
});
}),
pageviews: workspaceProcedure
.meta(
buildWebsiteOpenapi({
method: 'GET',
path: '/pageviews',
})
)
.input(
z
.object({
websiteId: z.string(),
startAt: z.number(),
endAt: z.number(),
unit: z.string().optional(),
})
.merge(websiteFilterSchema.partial())
)
.output(z.object({ pageviews: z.any(), sessions: z.any() }))
.query(async ({ input }) => {
const {
websiteId,
startAt,
endAt,
timezone,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
} = input;
const { startDate, endDate, unit } = await parseDateRange({
websiteId,
startAt: Number(startAt),
endAt: Number(endAt),
unit: String(input.unit),
});
const filters = {
startDate,
endDate,
timezone,
unit,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
};
const [pageviews, sessions] = await Promise.all([
2024-02-27 12:36:56 +00:00
getWorkspaceWebsitePageview(websiteId, filters as WebsiteQueryFilters),
getWorkspaceWebsiteSession(websiteId, filters as WebsiteQueryFilters),
]);
return {
pageviews,
sessions,
};
}),
2023-10-06 14:08:15 +00:00
metrics: workspaceProcedure
.meta(
buildWebsiteOpenapi({
method: 'GET',
path: '/metrics',
})
)
2023-10-06 14:08:15 +00:00
.input(
z.object({
websiteId: z.string(),
type: z.enum([
'url',
'language',
'referrer',
2024-05-07 16:26:04 +00:00
'title',
2023-10-06 14:08:15 +00:00
'browser',
'os',
'device',
'country',
'event',
]),
startAt: z.number(),
endAt: z.number(),
url: z.string().optional(),
referrer: z.string().optional(),
title: z.string().optional(),
os: z.string().optional(),
browser: z.string().optional(),
device: z.string().optional(),
country: z.string().optional(),
region: z.string().optional(),
city: z.string().optional(),
language: z.string().optional(),
event: z.string().optional(),
})
)
.output(
z.array(
z.object({
x: z.string().nullable(),
y: z.number(),
})
)
)
2023-10-06 14:08:15 +00:00
.query(async ({ input }) => {
const {
websiteId,
type,
startAt,
endAt,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
language,
event,
} = input;
const { startDate, endDate } = await parseDateRange({
websiteId,
startAt,
endAt,
});
const filters = {
startDate,
endDate,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
language,
event,
};
const column = FILTER_COLUMNS[type] || type;
if (SESSION_COLUMNS.includes(type)) {
2024-02-27 12:36:56 +00:00
const data = await getWebsiteSessionMetrics(websiteId, column, filters);
2023-10-06 14:08:15 +00:00
if (type === 'language') {
const combined: Record<string, any> = {};
for (const { x, y } of data) {
const key = String(x).toLowerCase().split('-')[0];
if (combined[key] === undefined) {
combined[key] = { x: key, y };
} else {
combined[key].y += y;
}
}
return Object.values(combined).map((d) => ({
x: d.x,
y: Number(d.y),
}));
2023-10-06 14:08:15 +00:00
}
return data.map((d) => ({ x: d.x, y: Number(d.y) }));
2023-10-06 14:08:15 +00:00
}
if (EVENT_COLUMNS.includes(type)) {
2024-02-27 12:36:56 +00:00
const data = await getWebsitePageviewMetrics(
websiteId,
column,
filters
);
2023-10-06 14:08:15 +00:00
return data.map((d) => ({ x: d.x, y: Number(d.y) }));
2023-10-06 14:08:15 +00:00
}
return [];
}),
add: workspaceOwnerProcedure
.meta({
openapi: {
method: 'POST',
tags: [OPENAPI_TAG.WEBSITE],
protect: true,
path: `/workspace/{workspaceId}/website/add`,
},
})
.input(
z.object({
name: websiteNameSchema,
domain: websiteDomainSchema,
})
)
.output(websiteInfoSchema)
.mutation(async ({ input }) => {
const { workspaceId, name, domain } = input;
const website = await prisma.website.create({
data: {
name,
domain,
workspaceId,
},
});
return website;
}),
updateInfo: workspaceOwnerProcedure
.meta(
buildWebsiteOpenapi({
method: 'PUT',
path: '/update',
})
)
.input(
z.object({
2023-10-14 16:51:03 +00:00
websiteId: z.string().cuid2(),
name: websiteNameSchema,
domain: websiteDomainSchema,
monitorId: z.string().cuid2().nullish(),
})
)
.output(websiteInfoSchema)
.mutation(async ({ input }) => {
2023-10-14 16:51:03 +00:00
const { workspaceId, websiteId, name, domain, monitorId } = input;
const websiteInfo = await prisma.website.update({
where: {
id: websiteId,
workspaceId,
},
data: {
name,
domain,
2023-10-14 16:51:03 +00:00
monitorId,
},
});
return websiteInfo;
}),
2023-09-28 10:01:04 +00:00
});
function buildWebsiteOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {
return {
openapi: {
tags: [OPENAPI_TAG.WEBSITE],
protect: true,
...meta,
path: `/workspace/{workspaceId}/website/{websiteId}${meta.path}`,
},
};
}