From dd0ad8c5deb217673285ada601c3121815eb37b8 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 17 Feb 2024 14:26:55 +0800 Subject: [PATCH] refactor: refactor website pageview endpoint to trpc --- src/client/api/model/website.ts | 47 -------- .../components/website/WebsiteOverview.tsx | 19 ++-- src/server/model/website.ts | 92 +++++++++++++++- src/server/model/workspace.ts | 101 ------------------ src/server/router/workspace.ts | 74 +------------ src/server/trpc/routers/website.ts | 86 +++++++++++++-- 6 files changed, 183 insertions(+), 236 deletions(-) diff --git a/src/client/api/model/website.ts b/src/client/api/model/website.ts index c2ee9c1..cc154cd 100644 --- a/src/client/api/model/website.ts +++ b/src/client/api/model/website.ts @@ -1,8 +1,5 @@ -import { useQuery } from '@tanstack/react-query'; -import { DateUnit } from '../../utils/date'; import { queryClient } from '../cache'; import { request } from '../request'; -import { getUserTimezone } from './user'; import { AppRouterOutput } from '../trpc'; export type WebsiteInfo = NonNullable; @@ -19,47 +16,3 @@ export async function deleteWorkspaceWebsite( export function refreshWorkspaceWebsites(workspaceId: string) { queryClient.refetchQueries(['websites', workspaceId]); } - -export async function getWorkspaceWebsitePageview( - workspaceId: string, - websiteId: string, - filter: Record -) { - const { data } = await request.get( - `/api/workspace/${workspaceId}/website/${websiteId}/pageviews`, - { - params: { - ...filter, - }, - } - ); - - return data; -} - -export function useWorkspaceWebsitePageview( - workspaceId: string, - websiteId: string, - startAt: number, - endAt: number, - unit: DateUnit -) { - const { data, isLoading, refetch } = useQuery( - ['websitePageview', { workspaceId, websiteId, startAt, endAt }], - () => { - return getWorkspaceWebsitePageview(workspaceId, websiteId, { - startAt, - endAt, - unit, - timezone: getUserTimezone(), - }); - } - ); - - return { - pageviews: data?.pageviews ?? [], - sessions: data?.sessions ?? [], - isLoading, - refetch, - }; -} diff --git a/src/client/components/website/WebsiteOverview.tsx b/src/client/components/website/WebsiteOverview.tsx index 5f66f8c..95bb421 100644 --- a/src/client/components/website/WebsiteOverview.tsx +++ b/src/client/components/website/WebsiteOverview.tsx @@ -40,17 +40,18 @@ export const WebsiteOverview: React.FC<{ ); const { - pageviews, - sessions, + data, isLoading: isLoadingPageview, refetch: refetchPageview, - } = useWorkspaceWebsitePageview( - website.workspaceId, - website.id, - startDate.unix() * 1000, - endDate.unix() * 1000, - unit - ); + } = trpc.website.pageviews.useQuery({ + workspaceId: website.workspaceId, + websiteId: website.id, + startAt: startDate.valueOf(), + endAt: endDate.valueOf(), + unit, + }); + const pageviews = data?.pageviews ?? []; + const sessions = data?.sessions ?? []; const { data: stats, diff --git a/src/server/model/website.ts b/src/server/model/website.ts index 7900deb..530dd00 100644 --- a/src/server/model/website.ts +++ b/src/server/model/website.ts @@ -12,7 +12,12 @@ import { } from '../utils/const'; import type { DynamicData } from '../utils/types'; import dayjs from 'dayjs'; -import { QueryFilters, parseFilters } from '../utils/prisma'; +import { + QueryFilters, + getDateQuery, + getTimestampIntervalQuery, + parseFilters, +} from '../utils/prisma'; export interface WebsiteEventPayload { data?: object; @@ -344,3 +349,88 @@ export async function getPageviewMetrics( limit 100 `; } + +export async function getWorkspaceWebsitePageview( + websiteId: string, + filters: QueryFilters +) { + const { timezone = 'utc', unit = 'day' } = filters; + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + }); + + return prisma.$queryRaw` + select + ${getDateQuery('"WebsiteEvent"."createdAt"', unit, timezone)} x, + count(1) y + from "WebsiteEvent" + ${joinSession} + where "WebsiteEvent"."websiteId" = ${params.websiteId} + and "WebsiteEvent"."createdAt" between ${ + params.startDate + }::timestamptz and ${params.endDate}::timestamptz + and "WebsiteEvent"."eventType" = ${EVENT_TYPE.pageView} + ${filterQuery} + group by 1 + `; +} + +export async function getWorkspaceWebsiteSession( + websiteId: string, + filters: QueryFilters +) { + const { timezone = 'utc', unit = 'day' } = filters; + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + }); + + return prisma.$queryRaw` + select + ${getDateQuery('"WebsiteEvent"."createdAt"', unit, timezone)} x, + count(distinct "WebsiteEvent"."sessionId") y + from "WebsiteEvent" + ${joinSession} + where "WebsiteEvent"."websiteId" = ${params.websiteId} + and "WebsiteEvent"."createdAt" between ${ + params.startDate + }::timestamptz and ${params.endDate}::timestamptz + and "WebsiteEvent"."eventType" = ${EVENT_TYPE.pageView} + ${filterQuery} + group by 1 + `; +} + +export async function getWorkspaceWebsiteStats( + websiteId: string, + filters: QueryFilters +): Promise { + const { filterQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + }); + + return prisma.$queryRaw` + select + sum(t.c) as "pageviews", + count(distinct t."sessionId") as "uniques", + sum(case when t.c = 1 then 1 else 0 end) as "bounces", + sum(t.time) as "totaltime" + from ( + select + "WebsiteEvent"."sessionId", + ${getDateQuery('"WebsiteEvent"."createdAt"', 'hour')}, + count(*) as c, + ${getTimestampIntervalQuery('"WebsiteEvent"."createdAt"')} as "time" + from "WebsiteEvent" + join "Website" + on "WebsiteEvent"."websiteId" = "Website"."id" + ${joinSession} + where "Website"."id" = ${params.websiteId} + and "WebsiteEvent"."createdAt" between ${ + params.startDate + }::timestamptz and ${params.endDate}::timestamptz + and "eventType" = ${EVENT_TYPE.pageView} + ${filterQuery} + group by 1, 2 + ) as t + `; +} diff --git a/src/server/model/workspace.ts b/src/server/model/workspace.ts index 00b2c53..e792849 100644 --- a/src/server/model/workspace.ts +++ b/src/server/model/workspace.ts @@ -52,22 +52,6 @@ export async function getWorkspaceWebsites(workspaceId: string) { return workspace?.websites ?? []; } -export async function addWorkspaceWebsite( - workspaceId: string, - name: string, - domain: string -) { - const website = await prisma.website.create({ - data: { - name, - domain, - workspaceId, - }, - }); - - return website; -} - export async function deleteWorkspaceWebsite( workspaceId: string, websiteId: string @@ -107,88 +91,3 @@ export async function getWorkspaceWebsiteDateRange(websiteId: string) { min: res._min.createdAt, }; } - -export async function getWorkspaceWebsitePageview( - websiteId: string, - filters: QueryFilters -) { - const { timezone = 'utc', unit = 'day' } = filters; - const { filterQuery, joinSession, params } = await parseFilters(websiteId, { - ...filters, - }); - - return prisma.$queryRaw` - select - ${getDateQuery('"WebsiteEvent"."createdAt"', unit, timezone)} x, - count(1) y - from "WebsiteEvent" - ${joinSession} - where "WebsiteEvent"."websiteId" = ${params.websiteId} - and "WebsiteEvent"."createdAt" between ${ - params.startDate - }::timestamptz and ${params.endDate}::timestamptz - and "WebsiteEvent"."eventType" = ${EVENT_TYPE.pageView} - ${filterQuery} - group by 1 - `; -} - -export async function getWorkspaceWebsiteSession( - websiteId: string, - filters: QueryFilters -) { - const { timezone = 'utc', unit = 'day' } = filters; - const { filterQuery, joinSession, params } = await parseFilters(websiteId, { - ...filters, - }); - - return prisma.$queryRaw` - select - ${getDateQuery('"WebsiteEvent"."createdAt"', unit, timezone)} x, - count(distinct "WebsiteEvent"."sessionId") y - from "WebsiteEvent" - ${joinSession} - where "WebsiteEvent"."websiteId" = ${params.websiteId} - and "WebsiteEvent"."createdAt" between ${ - params.startDate - }::timestamptz and ${params.endDate}::timestamptz - and "WebsiteEvent"."eventType" = ${EVENT_TYPE.pageView} - ${filterQuery} - group by 1 - `; -} - -export async function getWorkspaceWebsiteStats( - websiteId: string, - filters: QueryFilters -): Promise { - const { filterQuery, joinSession, params } = await parseFilters(websiteId, { - ...filters, - }); - - return prisma.$queryRaw` - select - sum(t.c) as "pageviews", - count(distinct t."sessionId") as "uniques", - sum(case when t.c = 1 then 1 else 0 end) as "bounces", - sum(t.time) as "totaltime" - from ( - select - "WebsiteEvent"."sessionId", - ${getDateQuery('"WebsiteEvent"."createdAt"', 'hour')}, - count(*) as c, - ${getTimestampIntervalQuery('"WebsiteEvent"."createdAt"')} as "time" - from "WebsiteEvent" - join "Website" - on "WebsiteEvent"."websiteId" = "Website"."id" - ${joinSession} - where "Website"."id" = ${params.websiteId} - and "WebsiteEvent"."createdAt" between ${ - params.startDate - }::timestamptz and ${params.endDate}::timestamptz - and "eventType" = ${EVENT_TYPE.pageView} - ${filterQuery} - group by 1, 2 - ) as t - `; -} diff --git a/src/server/router/workspace.ts b/src/server/router/workspace.ts index 76bfdf6..345f965 100644 --- a/src/server/router/workspace.ts +++ b/src/server/router/workspace.ts @@ -1,18 +1,8 @@ -import dayjs from 'dayjs'; import { Router } from 'express'; import { auth } from '../middleware/auth'; -import { body, param, query, validate } from '../middleware/validate'; +import { param, validate } from '../middleware/validate'; import { workspacePermission } from '../middleware/workspace'; -import { - addWorkspaceWebsite, - deleteWorkspaceWebsite, - getWorkspaceWebsitePageview, - getWorkspaceWebsites, - getWorkspaceWebsiteSession, - getWorkspaceWebsiteStats, -} from '../model/workspace'; -import { parseDateRange } from '../utils/common'; -import { QueryFilters } from '../utils/prisma'; +import { deleteWorkspaceWebsite } from '../model/workspace'; import { ROLES } from '@tianji/shared'; export const workspaceRouter = Router(); @@ -31,63 +21,3 @@ workspaceRouter.delete( res.json({ website }); } ); - -workspaceRouter.get( - '/:workspaceId/website/:websiteId/pageviews', - validate( - param('workspaceId').isString(), - param('websiteId').isString(), - query('startAt').isNumeric().withMessage('startAt should be number'), - query('endAt').isNumeric().withMessage('startAt should be number') - ), - auth(), - workspacePermission(), - async (req, res) => { - const workspaceId = req.params.workspaceId; - const websiteId = req.params.websiteId; - const { - timezone, - url, - referrer, - title, - os, - browser, - device, - country, - region, - city, - startAt, - endAt, - } = req.query; - - const { startDate, endDate, unit } = await parseDateRange({ - websiteId, - startAt: Number(startAt), - endAt: Number(endAt), - unit: String(req.query.unit), - }); - - const filters = { - startDate, - endDate, - timezone, - unit, - url, - referrer, - title, - os, - browser, - device, - country, - region, - city, - }; - - const [pageviews, sessions] = await Promise.all([ - getWorkspaceWebsitePageview(websiteId, filters as QueryFilters), - getWorkspaceWebsiteSession(websiteId, filters as QueryFilters), - ]); - - res.json({ pageviews, sessions }); - } -); diff --git a/src/server/trpc/routers/website.ts b/src/server/trpc/routers/website.ts index ecac631..c1c0144 100644 --- a/src/server/trpc/routers/website.ts +++ b/src/server/trpc/routers/website.ts @@ -5,7 +5,11 @@ import { workspaceProcedure, } from '../trpc'; import { z } from 'zod'; -import { getWebsiteOnlineUserCount } from '../../model/website'; +import { + getWebsiteOnlineUserCount, + getWorkspaceWebsitePageview, + getWorkspaceWebsiteStats, +} from '../../model/website'; import { prisma } from '../../model/_client'; import { EVENT_COLUMNS, @@ -18,10 +22,6 @@ import { getSessionMetrics, getPageviewMetrics } from '../../model/website'; import { websiteInfoSchema } from '../../model/_schema'; import { OpenApiMeta } from 'trpc-openapi'; import { hostnameRegex } from '@tianji/shared'; -import { - addWorkspaceWebsite, - getWorkspaceWebsiteStats, -} from '../../model/workspace'; import { websiteFilterSchema, websiteStatsSchema, @@ -241,6 +241,74 @@ export const websiteRouter = router({ }; }); }), + 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([ + getWorkspaceWebsitePageview(websiteId, filters as QueryFilters), + getWorkspaceWebsiteSession(websiteId, filters as QueryFilters), + ]); + + return { + pageviews, + sessions, + }; + }), metrics: workspaceProcedure .meta( buildWebsiteOpenapi({ @@ -379,7 +447,13 @@ export const websiteRouter = router({ .mutation(async ({ input }) => { const { workspaceId, name, domain } = input; - const website = await addWorkspaceWebsite(workspaceId, name, domain); + const website = await prisma.website.create({ + data: { + name, + domain, + workspaceId, + }, + }); return website; }),