feat: add telemetry metrics table
This commit is contained in:
parent
1306187f01
commit
38dd60feee
82
src/client/components/telemetry/TelemetryMetricsTable.tsx
Normal file
82
src/client/components/telemetry/TelemetryMetricsTable.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { Table } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/es/table/interface';
|
||||||
|
import React from 'react';
|
||||||
|
import { AppRouterOutput, trpc } from '../../api/trpc';
|
||||||
|
import { useCurrentWorkspaceId } from '../../store/user';
|
||||||
|
import { sum } from 'lodash-es';
|
||||||
|
import { formatNumber } from '../../utils/common';
|
||||||
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
|
||||||
|
type MetricsItemType = AppRouterOutput['telemetry']['metrics'][number];
|
||||||
|
|
||||||
|
interface MetricsTableProps {
|
||||||
|
telemetryId: string;
|
||||||
|
title: [string, string];
|
||||||
|
type: 'source' | 'url' | 'referrer' | 'country';
|
||||||
|
startAt: number;
|
||||||
|
endAt: number;
|
||||||
|
}
|
||||||
|
export const TelemetryMetricsTable: React.FC<MetricsTableProps> = React.memo(
|
||||||
|
(props) => {
|
||||||
|
const { telemetryId, title, type, startAt, endAt } = props;
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { isLoading, data: metrics = [] } = trpc.telemetry.metrics.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
telemetryId,
|
||||||
|
type,
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = sum(metrics.map((m) => m.y));
|
||||||
|
|
||||||
|
const columns: ColumnsType<MetricsItemType> = [
|
||||||
|
{
|
||||||
|
title: title[0],
|
||||||
|
dataIndex: 'x',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (val) =>
|
||||||
|
val ?? <span className="italic opacity-60">{t('(None)')}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: title[1],
|
||||||
|
dataIndex: 'y',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
render: (val) => {
|
||||||
|
const percent = (Number(val) / total) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-12 text-right">{formatNumber(val)}</div>
|
||||||
|
<div className="inline-block w-12 relative border-l ml-1 px-1">
|
||||||
|
<div
|
||||||
|
className="bg-blue-300 absolute h-full bg-opacity-25 left-0 top-0 pointer-events-none"
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
<span>{percent.toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
rowKey="x"
|
||||||
|
loading={isLoading}
|
||||||
|
dataSource={metrics}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
TelemetryMetricsTable.displayName = 'TelemetryMetricsTable';
|
@ -4,10 +4,18 @@ import { useParams } from 'react-router';
|
|||||||
import { NotFoundTip } from '../../components/NotFoundTip';
|
import { NotFoundTip } from '../../components/NotFoundTip';
|
||||||
import { useCurrentWorkspaceId } from '../../store/user';
|
import { useCurrentWorkspaceId } from '../../store/user';
|
||||||
import { TelemetryOverview } from '../../components/telemetry/TelemetryOverview';
|
import { TelemetryOverview } from '../../components/telemetry/TelemetryOverview';
|
||||||
|
import { TelemetryMetricsTable } from '../../components/telemetry/TelemetryMetricsTable';
|
||||||
|
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
|
||||||
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
|
||||||
export const TelemetryDetailPage: React.FC = React.memo(() => {
|
export const TelemetryDetailPage: React.FC = React.memo(() => {
|
||||||
const { telemetryId } = useParams();
|
const { telemetryId } = useParams();
|
||||||
const workspaceId = useCurrentWorkspaceId();
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { startDate, endDate } = useGlobalRangeDate();
|
||||||
|
|
||||||
|
const startAt = startDate.valueOf();
|
||||||
|
const endAt = endDate.valueOf();
|
||||||
|
|
||||||
if (!telemetryId) {
|
if (!telemetryId) {
|
||||||
return <NotFoundTip />;
|
return <NotFoundTip />;
|
||||||
@ -23,6 +31,26 @@ export const TelemetryDetailPage: React.FC = React.memo(() => {
|
|||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
/>
|
/>
|
||||||
</Card.Grid>
|
</Card.Grid>
|
||||||
|
|
||||||
|
<Card.Grid hoverable={false} className="!w-1/2 min-h-[470px]">
|
||||||
|
<TelemetryMetricsTable
|
||||||
|
telemetryId={telemetryId}
|
||||||
|
type="source"
|
||||||
|
title={[t('Source'), t('Views')]}
|
||||||
|
startAt={startAt}
|
||||||
|
endAt={endAt}
|
||||||
|
/>
|
||||||
|
</Card.Grid>
|
||||||
|
|
||||||
|
<Card.Grid hoverable={false} className="!w-1/2 min-h-[470px]">
|
||||||
|
<TelemetryMetricsTable
|
||||||
|
telemetryId={telemetryId}
|
||||||
|
type="country"
|
||||||
|
title={[t('Countries'), t('Visitors')]}
|
||||||
|
startAt={startAt}
|
||||||
|
endAt={endAt}
|
||||||
|
/>
|
||||||
|
</Card.Grid>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"k51bac044": "Abbrechen",
|
"k51bac044": "Abbrechen",
|
||||||
"k53ae02a5": "Neu",
|
"k53ae02a5": "Neu",
|
||||||
"k542b527c": "Ereignisse",
|
"k542b527c": "Ereignisse",
|
||||||
|
"k58267a45": "Source",
|
||||||
"k58f90514": "Bot-Token",
|
"k58f90514": "Bot-Token",
|
||||||
"k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?",
|
"k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?",
|
||||||
"k5a839f71": "Betriebszeit",
|
"k5a839f71": "Betriebszeit",
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"k51bac044": "Cancel",
|
"k51bac044": "Cancel",
|
||||||
"k53ae02a5": "New",
|
"k53ae02a5": "New",
|
||||||
"k542b527c": "Events",
|
"k542b527c": "Events",
|
||||||
|
"k58267a45": "Source",
|
||||||
"k58f90514": "Bot Token",
|
"k58f90514": "Bot Token",
|
||||||
"k593cf342": "Did you sure delete this monitor?",
|
"k593cf342": "Did you sure delete this monitor?",
|
||||||
"k5a839f71": "Uptime",
|
"k5a839f71": "Uptime",
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"k51bac044": "Annuler",
|
"k51bac044": "Annuler",
|
||||||
"k53ae02a5": "Nouveau",
|
"k53ae02a5": "Nouveau",
|
||||||
"k542b527c": "Événements",
|
"k542b527c": "Événements",
|
||||||
|
"k58267a45": "Source",
|
||||||
"k58f90514": "Jeton de bot",
|
"k58f90514": "Jeton de bot",
|
||||||
"k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?",
|
"k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?",
|
||||||
"k5a839f71": "Disponibilité",
|
"k5a839f71": "Disponibilité",
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"k51bac044": "キャンセル",
|
"k51bac044": "キャンセル",
|
||||||
"k53ae02a5": "新規",
|
"k53ae02a5": "新規",
|
||||||
"k542b527c": "イベント",
|
"k542b527c": "イベント",
|
||||||
|
"k58267a45": "Source",
|
||||||
"k58f90514": "ボットトークン",
|
"k58f90514": "ボットトークン",
|
||||||
"k593cf342": "このモニターを削除してもよろしいですか?",
|
"k593cf342": "このモニターを削除してもよろしいですか?",
|
||||||
"k5a839f71": "アップタイム",
|
"k5a839f71": "アップタイム",
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"k51bac044": "Отмена",
|
"k51bac044": "Отмена",
|
||||||
"k53ae02a5": "Новый",
|
"k53ae02a5": "Новый",
|
||||||
"k542b527c": "События",
|
"k542b527c": "События",
|
||||||
|
"k58267a45": "Source",
|
||||||
"k58f90514": "Токен бота",
|
"k58f90514": "Токен бота",
|
||||||
"k593cf342": "Вы уверены, что хотите удалить этот монитор?",
|
"k593cf342": "Вы уверены, что хотите удалить этот монитор?",
|
||||||
"k5a839f71": "Время работы",
|
"k5a839f71": "Время работы",
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
"k51bac044": "取消",
|
"k51bac044": "取消",
|
||||||
"k53ae02a5": "新建",
|
"k53ae02a5": "新建",
|
||||||
"k542b527c": "事件",
|
"k542b527c": "事件",
|
||||||
|
"k58267a45": "Source",
|
||||||
"k58f90514": "机器人令牌",
|
"k58f90514": "机器人令牌",
|
||||||
"k593cf342": "您确定要删除这个监控器吗?",
|
"k593cf342": "您确定要删除这个监控器吗?",
|
||||||
"k5a839f71": "正常运行时间",
|
"k5a839f71": "正常运行时间",
|
||||||
|
@ -295,7 +295,7 @@ export async function getTelemetryPageviewMetrics(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return prisma.$queryRaw`
|
return prisma.$queryRaw`
|
||||||
select ${Prisma.sql([`"${column}"`])} x, count(*) y
|
select ${Prisma.sql([`"${column}"`])} x, count(*) y
|
||||||
from "TelemetryEvent"
|
from "TelemetryEvent"
|
||||||
${joinSession}
|
${joinSession}
|
||||||
where "TelemetryEvent"."telemetryId" = ${telemetryId}
|
where "TelemetryEvent"."telemetryId" = ${telemetryId}
|
||||||
@ -309,3 +309,28 @@ export async function getTelemetryPageviewMetrics(
|
|||||||
limit 100
|
limit 100
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTelemetryUrlMetrics(
|
||||||
|
telemetryId: string,
|
||||||
|
filters: BaseQueryFilters
|
||||||
|
): Promise<{ x: string; y: number }[]> {
|
||||||
|
const { filterQuery, joinSession, params } = await parseTelemetryFilters(
|
||||||
|
telemetryId,
|
||||||
|
{
|
||||||
|
...filters,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return prisma.$queryRaw`
|
||||||
|
select CONCAT("urlOrigin", "urlPath") x, count(*) y
|
||||||
|
from "TelemetryEvent"
|
||||||
|
${joinSession}
|
||||||
|
where "TelemetryEvent"."telemetryId" = ${telemetryId}
|
||||||
|
and "TelemetryEvent"."createdAt"
|
||||||
|
between ${params.startDate}::timestamptz and ${params.endDate}::timestamptz
|
||||||
|
${filterQuery}
|
||||||
|
group by 1
|
||||||
|
order by 2 desc
|
||||||
|
limit 100
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
getTelemetrySession,
|
getTelemetrySession,
|
||||||
getTelemetrySessionMetrics,
|
getTelemetrySessionMetrics,
|
||||||
getTelemetryStats,
|
getTelemetryStats,
|
||||||
|
getTelemetryUrlMetrics,
|
||||||
} from '../../model/telemetry';
|
} from '../../model/telemetry';
|
||||||
import { BaseQueryFilters } from '../../utils/prisma';
|
import { BaseQueryFilters } from '../../utils/prisma';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@ -199,17 +200,8 @@ export const telemetryRouter = router({
|
|||||||
.input(
|
.input(
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
websiteId: z.string(),
|
telemetryId: z.string(),
|
||||||
type: z.enum([
|
type: z.enum(['source', 'url', 'referrer', 'country']),
|
||||||
'url',
|
|
||||||
'language',
|
|
||||||
'referrer',
|
|
||||||
'browser',
|
|
||||||
'os',
|
|
||||||
'device',
|
|
||||||
'country',
|
|
||||||
'event',
|
|
||||||
]),
|
|
||||||
startAt: z.number(),
|
startAt: z.number(),
|
||||||
endAt: z.number(),
|
endAt: z.number(),
|
||||||
})
|
})
|
||||||
@ -224,7 +216,7 @@ export const telemetryRouter = router({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { websiteId, type, startAt, endAt, url, country, region, city } =
|
const { telemetryId, type, startAt, endAt, url, country, region, city } =
|
||||||
input;
|
input;
|
||||||
|
|
||||||
const startDate = new Date(startAt);
|
const startDate = new Date(startAt);
|
||||||
@ -245,40 +237,27 @@ export const telemetryRouter = router({
|
|||||||
city,
|
city,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (type === 'source') {
|
||||||
|
const data = await getTelemetryUrlMetrics(telemetryId, filters);
|
||||||
|
|
||||||
|
return data.map((d) => ({ x: d.x, y: Number(d.y) }));
|
||||||
|
}
|
||||||
|
|
||||||
const column = FILTER_COLUMNS[type] || type;
|
const column = FILTER_COLUMNS[type] || type;
|
||||||
|
|
||||||
if (SESSION_COLUMNS.includes(type)) {
|
if (SESSION_COLUMNS.includes(type)) {
|
||||||
const data = await getTelemetrySessionMetrics(
|
const data = await getTelemetrySessionMetrics(
|
||||||
websiteId,
|
telemetryId,
|
||||||
column,
|
column,
|
||||||
filters
|
filters
|
||||||
);
|
);
|
||||||
|
|
||||||
if (type === 'language') {
|
|
||||||
const combined: Record<string, any> = {};
|
|
||||||
|
|
||||||
for (const { x, y } of data) {
|
|
||||||
const key = String(x).toLowerCase().split('-')[0];
|
|
||||||
|
|
||||||
if (combined[key] === undefined) {
|
|
||||||
combined[key] = { x: key, y };
|
|
||||||
} else {
|
|
||||||
combined[key].y += y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(combined).map((d) => ({
|
|
||||||
x: d.x,
|
|
||||||
y: Number(d.y),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.map((d) => ({ x: d.x, y: Number(d.y) }));
|
return data.map((d) => ({ x: d.x, y: Number(d.y) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EVENT_COLUMNS.includes(type)) {
|
if (EVENT_COLUMNS.includes(type)) {
|
||||||
const data = await getTelemetryPageviewMetrics(
|
const data = await getTelemetryPageviewMetrics(
|
||||||
websiteId,
|
telemetryId,
|
||||||
column,
|
column,
|
||||||
filters
|
filters
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user