tianji/src/server/model/website.ts

276 lines
6.2 KiB
TypeScript
Raw Normal View History

2023-09-03 11:28:53 +00:00
import { Website, WebsiteSession } from '@prisma/client';
2023-09-05 07:43:29 +00:00
import { flattenJSON, hashUuid, isUuid, parseToken } from '../utils/common';
2023-09-03 11:28:53 +00:00
import { prisma } from './_client';
import { Request } from 'express';
import { getClientInfo } from '../utils/detect';
import {
DATA_TYPE,
EVENT_NAME_LENGTH,
EVENT_TYPE,
URL_LENGTH,
} from '../utils/const';
import type { DynamicData } from '../utils/types';
2023-09-28 10:01:04 +00:00
import dayjs from 'dayjs';
2023-09-03 11:28:53 +00:00
export interface WebsiteEventPayload {
data?: object;
hostname: string;
language?: string;
referrer?: string;
screen?: string;
title?: string;
url?: string;
website: string;
name?: string;
}
export async function findSession(req: Request): Promise<{
id: any;
websiteId: string;
hostname: string;
browser: string;
os: any;
device: string;
screen: string;
language: string;
country: any;
subdivision1: any;
subdivision2: any;
city: any;
workspaceId: string;
}> {
// Verify payload
const { payload } = req.body;
2023-09-05 07:43:29 +00:00
// Check if cache token is passed
const cacheToken = req.headers['x-tianji-cache'] as string;
if (cacheToken) {
const result = parseToken(cacheToken);
if (result) {
return result as any;
}
}
2023-09-03 11:28:53 +00:00
const {
website: websiteId,
hostname,
screen,
language,
} = payload as WebsiteEventPayload;
// Check the hostname value for legality to eliminate dirty data
const validHostnameRegex = /^[\w-.]+$/;
if (typeof hostname === 'string' && !validHostnameRegex.test(hostname)) {
throw new Error('Invalid hostname.');
}
if (!isUuid(websiteId)) {
throw new Error('Invalid website ID.');
}
// Find website
const website = await loadWebsite(websiteId);
if (!website) {
throw new Error(`Website not found: ${websiteId}.`);
}
const {
userAgent,
browser,
os,
ip,
country,
subdivision1,
subdivision2,
city,
device,
} = await getClientInfo(req, payload);
const sessionId = hashUuid(websiteId, hostname!, ip, userAgent!);
// Find session
let session = await loadSession(sessionId);
// Create a session if not found
if (!session) {
try {
session = await prisma.websiteSession.create({
data: {
id: sessionId,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
},
});
} catch (e: any) {
if (!e.message.toLowerCase().includes('unique constraint')) {
throw e;
}
}
}
const res: any = { ...session!, workspaceId: website.workspaceId };
return res;
}
2023-09-12 15:04:39 +00:00
export async function loadWebsite(websiteId: string): Promise<Website | null> {
2023-09-03 11:28:53 +00:00
const website = await prisma.website.findUnique({
where: {
id: websiteId,
},
});
if (!website || website.deletedAt) {
return null;
}
return website;
}
async function loadSession(sessionId: string): Promise<WebsiteSession | null> {
const session = await prisma.websiteSession.findUnique({
where: {
id: sessionId,
},
});
if (!session) {
return null;
}
return session;
}
export async function saveWebsiteEvent(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 websiteEvent = await prisma.websiteEvent.create({
data: {
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) {
const jsonKeys = flattenJSON(eventData);
// id, websiteEventId, eventStringValue
const flattendData = jsonKeys.map((a) => ({
websiteEventId: websiteEvent.id,
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,
}));
await prisma.websiteEventData.createMany({
data: flattendData,
});
}
return websiteEvent;
}
export async function saveWebsiteSessionData(data: {
websiteId: string;
sessionId: string;
sessionData: DynamicData;
}) {
const { websiteId, sessionId, sessionData } = data;
const jsonKeys = flattenJSON(sessionData);
const flattendData = jsonKeys.map((a) => ({
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 prisma.$transaction([
prisma.websiteSessionData.deleteMany({
where: {
sessionId,
},
}),
prisma.websiteSessionData.createMany({
data: flattendData,
}),
]);
}
2023-09-28 10:01:04 +00:00
export async function getWebsiteOnlineUserCount(
websiteId: string
): Promise<number> {
const startAt = dayjs().subtract(5, 'minutes').toDate();
interface Ret {
x: number;
}
const res = await prisma.$queryRaw<
Ret[]
>`SELECT count(distinct "sessionId") x FROM "WebsiteEvent" where "websiteId" = ${websiteId}::uuid AND "createdAt" >= ${startAt}`;
console.log('res', res);
return res?.[0].x ?? 0;
}