refactor: change website stat endpoint to trpc

This commit is contained in:
moonrailgun 2024-01-29 19:56:13 +08:00
parent f153145e0d
commit 537edcf506
5 changed files with 136 additions and 156 deletions

View File

@ -63,59 +63,3 @@ export function useWorkspaceWebsitePageview(
refetch,
};
}
export interface StatsItemType {
value: number;
change: number;
}
export async function getWorkspaceWebsiteStats(
workspaceId: string,
websiteId: string,
filter: Record<string, any>
): Promise<{
bounces: StatsItemType;
pageviews: StatsItemType;
totaltime: StatsItemType;
uniques: StatsItemType;
}> {
const { data } = await request.get(
`/api/workspace/${workspaceId}/website/${websiteId}/stats`,
{
params: {
...filter,
},
}
);
return data.stats;
}
export function useWorkspaceWebsiteStats(
workspaceId: string,
websiteId: string,
startAt: number,
endAt: number,
unit: DateUnit
) {
const {
data: stats,
isLoading,
refetch,
} = useQuery(
['websiteStats', { workspaceId, websiteId, startAt, endAt }],
() => {
return getWorkspaceWebsiteStats(workspaceId, websiteId, {
startAt,
endAt,
unit,
timezone: getUserTimezone(),
});
}
);
return {
stats,
isLoading,
refetch,
};
}

View File

@ -4,9 +4,7 @@ import { Column, ColumnConfig } from '@ant-design/charts';
import { SyncOutlined } from '@ant-design/icons';
import { DateFilter } from '../DateFilter';
import {
StatsItemType,
useWorkspaceWebsitePageview,
useWorkspaceWebsiteStats,
WebsiteInfo,
} from '../../api/model/website';
import {
@ -23,6 +21,8 @@ import { WebsiteOnlineCount } from './WebsiteOnlineCount';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
import { MonitorHealthBar } from '../monitor/MonitorHealthBar';
import { useNavigate } from 'react-router';
import { AppRouterOutput, trpc } from '../../api/trpc';
import { getUserTimezone } from '../../api/model/user';
export const WebsiteOverview: React.FC<{
website: WebsiteInfo;
@ -47,16 +47,17 @@ export const WebsiteOverview: React.FC<{
);
const {
stats,
data: stats,
isLoading: isLoadingStats,
refetch: refetchStats,
} = useWorkspaceWebsiteStats(
website.workspaceId,
website.id,
startDate.unix() * 1000,
endDate.unix() * 1000,
unit
);
} = trpc.website.stats.useQuery({
workspaceId: website.workspaceId,
websiteId: website.id,
startAt: startDate.unix() * 1000,
endAt: endDate.unix() * 1000,
timezone: getUserTimezone(),
unit,
});
const handleRefresh = useEvent(async () => {
refresh();
@ -109,8 +110,8 @@ export const WebsiteOverview: React.FC<{
<div>{actions}</div>
</div>
<div className="flex mb-10 flex-wrap">
{stats && <MetricsBar stats={stats} />}
<div className="flex mb-10 flex-wrap justify-between">
<div className="flex-1">{stats && <MetricsBar stats={stats} />}</div>
<div className="flex items-center gap-2 justify-end w-full lg:w-1/3">
<Button
@ -136,12 +137,7 @@ export const WebsiteOverview: React.FC<{
WebsiteOverview.displayName = 'WebsiteOverview';
export const MetricsBar: React.FC<{
stats: {
bounces: StatsItemType;
pageviews: StatsItemType;
totaltime: StatsItemType;
uniques: StatsItemType;
};
stats: AppRouterOutput['website']['stats'];
}> = React.memo((props) => {
const { pageviews, uniques, bounces, totaltime } = props.stats || {};
const num = Math.min(uniques.value, bounces.value);
@ -153,7 +149,7 @@ export const MetricsBar: React.FC<{
};
return (
<div className="flex gap-5 flex-wrap w-full lg:w-2/3">
<div className="flex gap-5 flex-wrap w-full">
<MetricCard
label="Views"
value={pageviews.value}

View File

@ -0,0 +1,26 @@
import { z } from 'zod';
export const websiteFilterSchema = z.object({
timezone: z.string(),
url: z.string(),
referrer: z.string(),
title: z.string(),
os: z.string(),
browser: z.string(),
device: z.string(),
country: z.string(),
region: z.string(),
city: z.string(),
});
const websiteStatsItemType = z.object({
value: z.number(),
change: z.number(),
});
export const websiteStatsSchema = z.object({
bounces: websiteStatsItemType,
pageviews: websiteStatsItemType,
totaltime: websiteStatsItemType,
uniques: websiteStatsItemType,
});

View File

@ -91,83 +91,3 @@ workspaceRouter.get(
res.json({ pageviews, sessions });
}
);
workspaceRouter.get(
'/:workspaceId/website/:websiteId/stats',
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 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,
} as QueryFilters;
const [metrics, prevPeriod] = await Promise.all([
getWorkspaceWebsiteStats(websiteId, {
...filters,
startDate,
endDate,
}),
getWorkspaceWebsiteStats(websiteId, {
...filters,
startDate: prevStartDate,
endDate: prevEndDate,
}),
]);
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = {
value: Number(metrics[0][key]) || 0,
change: Number(metrics[0][key]) - Number(prevPeriod[0][key]) || 0,
};
return obj;
}, {} as Record<string, { value: number; change: number }>);
res.json({ stats });
}
);

View File

@ -18,7 +18,16 @@ import { getSessionMetrics, getPageviewMetrics } from '../../model/website';
import { websiteInfoSchema } from '../../model/_schema';
import { OpenApiMeta } from 'trpc-openapi';
import { hostnameRegex } from '@tianji/shared';
import { addWorkspaceWebsite } from '../../model/workspace';
import {
addWorkspaceWebsite,
getWorkspaceWebsiteStats,
} from '../../model/workspace';
import {
websiteFilterSchema,
websiteStatsSchema,
} from '../../model/_schema/filter';
import dayjs from 'dayjs';
import { QueryFilters } from '../../utils/prisma';
const websiteNameSchema = z.string().max(100);
const websiteDomainSchema = z.union([
@ -93,6 +102,91 @@ export const websiteRouter = router({
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,
} as QueryFilters;
const [metrics, prevPeriod] = await Promise.all([
getWorkspaceWebsiteStats(websiteId, {
...filters,
startDate,
endDate,
}),
getWorkspaceWebsiteStats(websiteId, {
...filters,
startDate: prevStartDate,
endDate: prevEndDate,
}),
]);
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = {
value: Number(metrics[0][key]) || 0,
change: Number(metrics[0][key]) - Number(prevPeriod[0][key]) || 0,
};
return obj;
}, {} as Record<string, { value: number; change: number }>);
return websiteStatsSchema.parse(stats);
}),
metrics: workspaceProcedure
.meta(
buildWebsiteOpenapi({