From 38dd60feeec35142cd08e4fe4fe2151b5acee400 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Mon, 4 Mar 2024 00:48:51 +0800 Subject: [PATCH] feat: add telemetry metrics table --- .../telemetry/TelemetryMetricsTable.tsx | 82 +++++++++++++++++++ src/client/pages/Telemetry/Detail.tsx | 28 +++++++ src/client/public/locales/de/translation.json | 1 + src/client/public/locales/en/translation.json | 1 + src/client/public/locales/fr/translation.json | 1 + src/client/public/locales/jp/translation.json | 1 + src/client/public/locales/ru/translation.json | 1 + src/client/public/locales/zh/translation.json | 1 + src/server/model/telemetry.ts | 27 +++++- src/server/trpc/routers/telemetry.ts | 45 +++------- 10 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 src/client/components/telemetry/TelemetryMetricsTable.tsx diff --git a/src/client/components/telemetry/TelemetryMetricsTable.tsx b/src/client/components/telemetry/TelemetryMetricsTable.tsx new file mode 100644 index 0000000..d43e6d7 --- /dev/null +++ b/src/client/components/telemetry/TelemetryMetricsTable.tsx @@ -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 = 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 = [ + { + title: title[0], + dataIndex: 'x', + ellipsis: true, + render: (val) => + val ?? {t('(None)')}, + }, + { + title: title[1], + dataIndex: 'y', + width: 100, + align: 'center', + render: (val) => { + const percent = (Number(val) / total) * 100; + + return ( +
+
{formatNumber(val)}
+
+
+ {percent.toFixed(0)}% +
+
+ ); + }, + }, + ]; + + return ( + + ); + } +); +TelemetryMetricsTable.displayName = 'TelemetryMetricsTable'; diff --git a/src/client/pages/Telemetry/Detail.tsx b/src/client/pages/Telemetry/Detail.tsx index 5ef6b4e..b3bc091 100644 --- a/src/client/pages/Telemetry/Detail.tsx +++ b/src/client/pages/Telemetry/Detail.tsx @@ -4,10 +4,18 @@ import { useParams } from 'react-router'; import { NotFoundTip } from '../../components/NotFoundTip'; import { useCurrentWorkspaceId } from '../../store/user'; 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(() => { const { telemetryId } = useParams(); const workspaceId = useCurrentWorkspaceId(); + const { t } = useTranslation(); + const { startDate, endDate } = useGlobalRangeDate(); + + const startAt = startDate.valueOf(); + const endAt = endDate.valueOf(); if (!telemetryId) { return ; @@ -23,6 +31,26 @@ export const TelemetryDetailPage: React.FC = React.memo(() => { workspaceId={workspaceId} /> + + + + + + + + ); diff --git a/src/client/public/locales/de/translation.json b/src/client/public/locales/de/translation.json index f2ac2c7..e46cb99 100644 --- a/src/client/public/locales/de/translation.json +++ b/src/client/public/locales/de/translation.json @@ -66,6 +66,7 @@ "k51bac044": "Abbrechen", "k53ae02a5": "Neu", "k542b527c": "Ereignisse", + "k58267a45": "Source", "k58f90514": "Bot-Token", "k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?", "k5a839f71": "Betriebszeit", diff --git a/src/client/public/locales/en/translation.json b/src/client/public/locales/en/translation.json index f0b78b3..c635073 100644 --- a/src/client/public/locales/en/translation.json +++ b/src/client/public/locales/en/translation.json @@ -66,6 +66,7 @@ "k51bac044": "Cancel", "k53ae02a5": "New", "k542b527c": "Events", + "k58267a45": "Source", "k58f90514": "Bot Token", "k593cf342": "Did you sure delete this monitor?", "k5a839f71": "Uptime", diff --git a/src/client/public/locales/fr/translation.json b/src/client/public/locales/fr/translation.json index e825589..7bb15ad 100644 --- a/src/client/public/locales/fr/translation.json +++ b/src/client/public/locales/fr/translation.json @@ -66,6 +66,7 @@ "k51bac044": "Annuler", "k53ae02a5": "Nouveau", "k542b527c": "Événements", + "k58267a45": "Source", "k58f90514": "Jeton de bot", "k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?", "k5a839f71": "Disponibilité", diff --git a/src/client/public/locales/jp/translation.json b/src/client/public/locales/jp/translation.json index c87312f..b8da43a 100644 --- a/src/client/public/locales/jp/translation.json +++ b/src/client/public/locales/jp/translation.json @@ -66,6 +66,7 @@ "k51bac044": "キャンセル", "k53ae02a5": "新規", "k542b527c": "イベント", + "k58267a45": "Source", "k58f90514": "ボットトークン", "k593cf342": "このモニターを削除してもよろしいですか?", "k5a839f71": "アップタイム", diff --git a/src/client/public/locales/ru/translation.json b/src/client/public/locales/ru/translation.json index 15b40b5..d346baa 100644 --- a/src/client/public/locales/ru/translation.json +++ b/src/client/public/locales/ru/translation.json @@ -66,6 +66,7 @@ "k51bac044": "Отмена", "k53ae02a5": "Новый", "k542b527c": "События", + "k58267a45": "Source", "k58f90514": "Токен бота", "k593cf342": "Вы уверены, что хотите удалить этот монитор?", "k5a839f71": "Время работы", diff --git a/src/client/public/locales/zh/translation.json b/src/client/public/locales/zh/translation.json index b045dfe..29e9a80 100644 --- a/src/client/public/locales/zh/translation.json +++ b/src/client/public/locales/zh/translation.json @@ -66,6 +66,7 @@ "k51bac044": "取消", "k53ae02a5": "新建", "k542b527c": "事件", + "k58267a45": "Source", "k58f90514": "机器人令牌", "k593cf342": "您确定要删除这个监控器吗?", "k5a839f71": "正常运行时间", diff --git a/src/server/model/telemetry.ts b/src/server/model/telemetry.ts index ba82917..9f80b5a 100644 --- a/src/server/model/telemetry.ts +++ b/src/server/model/telemetry.ts @@ -295,7 +295,7 @@ export async function getTelemetryPageviewMetrics( ); return prisma.$queryRaw` - select ${Prisma.sql([`"${column}"`])} x, count(*) y + select ${Prisma.sql([`"${column}"`])} x, count(*) y from "TelemetryEvent" ${joinSession} where "TelemetryEvent"."telemetryId" = ${telemetryId} @@ -309,3 +309,28 @@ export async function getTelemetryPageviewMetrics( 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 + `; +} diff --git a/src/server/trpc/routers/telemetry.ts b/src/server/trpc/routers/telemetry.ts index dc05e44..6e9e8b9 100644 --- a/src/server/trpc/routers/telemetry.ts +++ b/src/server/trpc/routers/telemetry.ts @@ -25,6 +25,7 @@ import { getTelemetrySession, getTelemetrySessionMetrics, getTelemetryStats, + getTelemetryUrlMetrics, } from '../../model/telemetry'; import { BaseQueryFilters } from '../../utils/prisma'; import dayjs from 'dayjs'; @@ -199,17 +200,8 @@ export const telemetryRouter = router({ .input( z .object({ - websiteId: z.string(), - type: z.enum([ - 'url', - 'language', - 'referrer', - 'browser', - 'os', - 'device', - 'country', - 'event', - ]), + telemetryId: z.string(), + type: z.enum(['source', 'url', 'referrer', 'country']), startAt: z.number(), endAt: z.number(), }) @@ -224,7 +216,7 @@ export const telemetryRouter = router({ ) ) .query(async ({ input }) => { - const { websiteId, type, startAt, endAt, url, country, region, city } = + const { telemetryId, type, startAt, endAt, url, country, region, city } = input; const startDate = new Date(startAt); @@ -245,40 +237,27 @@ export const telemetryRouter = router({ 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; if (SESSION_COLUMNS.includes(type)) { const data = await getTelemetrySessionMetrics( - websiteId, + telemetryId, column, filters ); - if (type === 'language') { - const combined: Record = {}; - - 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) })); } if (EVENT_COLUMNS.includes(type)) { const data = await getTelemetryPageviewMetrics( - websiteId, + telemetryId, column, filters );