refactor: change website stat endpoint to trpc
This commit is contained in:
parent
f153145e0d
commit
537edcf506
@ -63,59 +63,3 @@ export function useWorkspaceWebsitePageview(
|
|||||||
refetch,
|
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -4,9 +4,7 @@ import { Column, ColumnConfig } from '@ant-design/charts';
|
|||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import { DateFilter } from '../DateFilter';
|
import { DateFilter } from '../DateFilter';
|
||||||
import {
|
import {
|
||||||
StatsItemType,
|
|
||||||
useWorkspaceWebsitePageview,
|
useWorkspaceWebsitePageview,
|
||||||
useWorkspaceWebsiteStats,
|
|
||||||
WebsiteInfo,
|
WebsiteInfo,
|
||||||
} from '../../api/model/website';
|
} from '../../api/model/website';
|
||||||
import {
|
import {
|
||||||
@ -23,6 +21,8 @@ import { WebsiteOnlineCount } from './WebsiteOnlineCount';
|
|||||||
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
|
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
|
||||||
import { MonitorHealthBar } from '../monitor/MonitorHealthBar';
|
import { MonitorHealthBar } from '../monitor/MonitorHealthBar';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
import { AppRouterOutput, trpc } from '../../api/trpc';
|
||||||
|
import { getUserTimezone } from '../../api/model/user';
|
||||||
|
|
||||||
export const WebsiteOverview: React.FC<{
|
export const WebsiteOverview: React.FC<{
|
||||||
website: WebsiteInfo;
|
website: WebsiteInfo;
|
||||||
@ -47,16 +47,17 @@ export const WebsiteOverview: React.FC<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
stats,
|
data: stats,
|
||||||
isLoading: isLoadingStats,
|
isLoading: isLoadingStats,
|
||||||
refetch: refetchStats,
|
refetch: refetchStats,
|
||||||
} = useWorkspaceWebsiteStats(
|
} = trpc.website.stats.useQuery({
|
||||||
website.workspaceId,
|
workspaceId: website.workspaceId,
|
||||||
website.id,
|
websiteId: website.id,
|
||||||
startDate.unix() * 1000,
|
startAt: startDate.unix() * 1000,
|
||||||
endDate.unix() * 1000,
|
endAt: endDate.unix() * 1000,
|
||||||
unit
|
timezone: getUserTimezone(),
|
||||||
);
|
unit,
|
||||||
|
});
|
||||||
|
|
||||||
const handleRefresh = useEvent(async () => {
|
const handleRefresh = useEvent(async () => {
|
||||||
refresh();
|
refresh();
|
||||||
@ -109,8 +110,8 @@ export const WebsiteOverview: React.FC<{
|
|||||||
<div>{actions}</div>
|
<div>{actions}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex mb-10 flex-wrap">
|
<div className="flex mb-10 flex-wrap justify-between">
|
||||||
{stats && <MetricsBar stats={stats} />}
|
<div className="flex-1">{stats && <MetricsBar stats={stats} />}</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 justify-end w-full lg:w-1/3">
|
<div className="flex items-center gap-2 justify-end w-full lg:w-1/3">
|
||||||
<Button
|
<Button
|
||||||
@ -136,12 +137,7 @@ export const WebsiteOverview: React.FC<{
|
|||||||
WebsiteOverview.displayName = 'WebsiteOverview';
|
WebsiteOverview.displayName = 'WebsiteOverview';
|
||||||
|
|
||||||
export const MetricsBar: React.FC<{
|
export const MetricsBar: React.FC<{
|
||||||
stats: {
|
stats: AppRouterOutput['website']['stats'];
|
||||||
bounces: StatsItemType;
|
|
||||||
pageviews: StatsItemType;
|
|
||||||
totaltime: StatsItemType;
|
|
||||||
uniques: StatsItemType;
|
|
||||||
};
|
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
const { pageviews, uniques, bounces, totaltime } = props.stats || {};
|
const { pageviews, uniques, bounces, totaltime } = props.stats || {};
|
||||||
const num = Math.min(uniques.value, bounces.value);
|
const num = Math.min(uniques.value, bounces.value);
|
||||||
@ -153,7 +149,7 @@ export const MetricsBar: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-5 flex-wrap w-full lg:w-2/3">
|
<div className="flex gap-5 flex-wrap w-full">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Views"
|
label="Views"
|
||||||
value={pageviews.value}
|
value={pageviews.value}
|
||||||
|
26
src/server/model/_schema/filter.ts
Normal file
26
src/server/model/_schema/filter.ts
Normal 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,
|
||||||
|
});
|
@ -91,83 +91,3 @@ workspaceRouter.get(
|
|||||||
res.json({ pageviews, sessions });
|
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 });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
@ -18,7 +18,16 @@ 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 } 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 websiteNameSchema = z.string().max(100);
|
||||||
const websiteDomainSchema = z.union([
|
const websiteDomainSchema = z.union([
|
||||||
@ -93,6 +102,91 @@ export const websiteRouter = router({
|
|||||||
|
|
||||||
return website;
|
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
|
metrics: workspaceProcedure
|
||||||
.meta(
|
.meta(
|
||||||
buildWebsiteOpenapi({
|
buildWebsiteOpenapi({
|
||||||
|
Loading…
Reference in New Issue
Block a user