refactor: refactor website pageview endpoint to trpc

This commit is contained in:
moonrailgun 2024-02-17 14:26:55 +08:00
parent d3df3f2692
commit dd0ad8c5de
6 changed files with 183 additions and 236 deletions

View File

@ -1,8 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { DateUnit } from '../../utils/date';
import { queryClient } from '../cache'; import { queryClient } from '../cache';
import { request } from '../request'; import { request } from '../request';
import { getUserTimezone } from './user';
import { AppRouterOutput } from '../trpc'; import { AppRouterOutput } from '../trpc';
export type WebsiteInfo = NonNullable<AppRouterOutput['website']['info']>; export type WebsiteInfo = NonNullable<AppRouterOutput['website']['info']>;
@ -19,47 +16,3 @@ export async function deleteWorkspaceWebsite(
export function refreshWorkspaceWebsites(workspaceId: string) { export function refreshWorkspaceWebsites(workspaceId: string) {
queryClient.refetchQueries(['websites', workspaceId]); queryClient.refetchQueries(['websites', workspaceId]);
} }
export async function getWorkspaceWebsitePageview(
workspaceId: string,
websiteId: string,
filter: Record<string, any>
) {
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,
};
}

View File

@ -40,17 +40,18 @@ export const WebsiteOverview: React.FC<{
); );
const { const {
pageviews, data,
sessions,
isLoading: isLoadingPageview, isLoading: isLoadingPageview,
refetch: refetchPageview, refetch: refetchPageview,
} = useWorkspaceWebsitePageview( } = trpc.website.pageviews.useQuery({
website.workspaceId, workspaceId: website.workspaceId,
website.id, websiteId: website.id,
startDate.unix() * 1000, startAt: startDate.valueOf(),
endDate.unix() * 1000, endAt: endDate.valueOf(),
unit unit,
); });
const pageviews = data?.pageviews ?? [];
const sessions = data?.sessions ?? [];
const { const {
data: stats, data: stats,

View File

@ -12,7 +12,12 @@ import {
} from '../utils/const'; } from '../utils/const';
import type { DynamicData } from '../utils/types'; import type { DynamicData } from '../utils/types';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { QueryFilters, parseFilters } from '../utils/prisma'; import {
QueryFilters,
getDateQuery,
getTimestampIntervalQuery,
parseFilters,
} from '../utils/prisma';
export interface WebsiteEventPayload { export interface WebsiteEventPayload {
data?: object; data?: object;
@ -344,3 +349,88 @@ export async function getPageviewMetrics(
limit 100 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<any> {
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
`;
}

View File

@ -52,22 +52,6 @@ export async function getWorkspaceWebsites(workspaceId: string) {
return workspace?.websites ?? []; 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( export async function deleteWorkspaceWebsite(
workspaceId: string, workspaceId: string,
websiteId: string websiteId: string
@ -107,88 +91,3 @@ export async function getWorkspaceWebsiteDateRange(websiteId: string) {
min: res._min.createdAt, 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<any> {
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
`;
}

View File

@ -1,18 +1,8 @@
import dayjs from 'dayjs';
import { Router } from 'express'; import { Router } from 'express';
import { auth } from '../middleware/auth'; 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 { workspacePermission } from '../middleware/workspace';
import { import { deleteWorkspaceWebsite } from '../model/workspace';
addWorkspaceWebsite,
deleteWorkspaceWebsite,
getWorkspaceWebsitePageview,
getWorkspaceWebsites,
getWorkspaceWebsiteSession,
getWorkspaceWebsiteStats,
} from '../model/workspace';
import { parseDateRange } from '../utils/common';
import { QueryFilters } from '../utils/prisma';
import { ROLES } from '@tianji/shared'; import { ROLES } from '@tianji/shared';
export const workspaceRouter = Router(); export const workspaceRouter = Router();
@ -31,63 +21,3 @@ workspaceRouter.delete(
res.json({ website }); 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 });
}
);

View File

@ -5,7 +5,11 @@ import {
workspaceProcedure, workspaceProcedure,
} from '../trpc'; } from '../trpc';
import { z } from 'zod'; import { z } from 'zod';
import { getWebsiteOnlineUserCount } from '../../model/website'; import {
getWebsiteOnlineUserCount,
getWorkspaceWebsitePageview,
getWorkspaceWebsiteStats,
} from '../../model/website';
import { prisma } from '../../model/_client'; import { prisma } from '../../model/_client';
import { import {
EVENT_COLUMNS, EVENT_COLUMNS,
@ -18,10 +22,6 @@ import { getSessionMetrics, getPageviewMetrics } from '../../model/website';
import { websiteInfoSchema } from '../../model/_schema'; import { websiteInfoSchema } from '../../model/_schema';
import { OpenApiMeta } from 'trpc-openapi'; import { OpenApiMeta } from 'trpc-openapi';
import { hostnameRegex } from '@tianji/shared'; import { hostnameRegex } from '@tianji/shared';
import {
addWorkspaceWebsite,
getWorkspaceWebsiteStats,
} from '../../model/workspace';
import { import {
websiteFilterSchema, websiteFilterSchema,
websiteStatsSchema, 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 metrics: workspaceProcedure
.meta( .meta(
buildWebsiteOpenapi({ buildWebsiteOpenapi({
@ -379,7 +447,13 @@ export const websiteRouter = router({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { workspaceId, name, domain } = 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; return website;
}), }),