refactor: refactor website pageview endpoint to trpc
This commit is contained in:
parent
d3df3f2692
commit
dd0ad8c5de
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
@ -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 });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
@ -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;
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user