diff --git a/src/client/components/website/MetricCard.tsx b/src/client/components/MetricCard.tsx
similarity index 93%
rename from src/client/components/website/MetricCard.tsx
rename to src/client/components/MetricCard.tsx
index f55eb96..10acf4b 100644
--- a/src/client/components/website/MetricCard.tsx
+++ b/src/client/components/MetricCard.tsx
@@ -1,7 +1,7 @@
import { Tag } from 'antd';
import React from 'react';
-import { formatNumber } from '../../utils/common';
-import { useGlobalStateStore } from '../../store/global';
+import { formatNumber } from '../utils/common';
+import { useGlobalStateStore } from '../store/global';
import { useTranslation } from '@i18next-toolkit/react';
interface MetricCardProps {
diff --git a/src/client/components/telemetry/TelemetryOverview.tsx b/src/client/components/telemetry/TelemetryOverview.tsx
new file mode 100644
index 0000000..c95a94e
--- /dev/null
+++ b/src/client/components/telemetry/TelemetryOverview.tsx
@@ -0,0 +1,156 @@
+import { Button, message, Spin, Switch } from 'antd';
+import React from 'react';
+import { SyncOutlined } from '@ant-design/icons';
+import { DateFilter } from '../DateFilter';
+import { getDateArray } from '../../utils/date';
+import { useEvent } from '../../hooks/useEvent';
+import { MetricCard } from '../MetricCard';
+import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
+import { AppRouterOutput, trpc } from '../../api/trpc';
+import { getUserTimezone } from '../../api/model/user';
+import { useGlobalStateStore } from '../../store/global';
+import { useTranslation } from '@i18next-toolkit/react';
+import { TimeEventChart } from '../TimeEventChart';
+
+export const TelemetryOverview: React.FC<{
+ workspaceId: string;
+ telemetryId: string;
+ showDateFilter?: boolean;
+ actions?: React.ReactNode;
+}> = React.memo((props) => {
+ const { t } = useTranslation();
+ const { workspaceId, telemetryId, showDateFilter = false, actions } = props;
+ const { startDate, endDate, unit, refresh } = useGlobalRangeDate();
+ const showPreviousPeriod = useGlobalStateStore(
+ (state) => state.showPreviousPeriod
+ );
+
+ const { data: info } = trpc.telemetry.info.useQuery({
+ workspaceId,
+ telemetryId,
+ });
+
+ const {
+ data: chartData = [],
+ isLoading: isLoadingPageview,
+ refetch: refetchPageview,
+ } = trpc.telemetry.pageviews.useQuery(
+ {
+ workspaceId,
+ telemetryId,
+ startAt: startDate.valueOf(),
+ endAt: endDate.valueOf(),
+ unit,
+ timezone: getUserTimezone(),
+ },
+ {
+ select(data) {
+ const pageviews = data.pageviews ?? [];
+ const sessions = data.sessions ?? [];
+
+ const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
+ const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
+
+ return [
+ ...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })),
+ ...sessionsArr.map((item) => ({ ...item, type: 'session' })),
+ ];
+ },
+ }
+ );
+
+ const {
+ data: stats,
+ isLoading: isLoadingStats,
+ refetch: refetchStats,
+ } = trpc.telemetry.stats.useQuery({
+ workspaceId,
+ telemetryId,
+ startAt: startDate.unix() * 1000,
+ endAt: endDate.unix() * 1000,
+ timezone: getUserTimezone(),
+ unit,
+ });
+
+ const handleRefresh = useEvent(async () => {
+ refresh();
+
+ await Promise.all([refetchPageview(), refetchStats()]);
+
+ message.success(t('Refreshed'));
+ });
+
+ const loading = isLoadingPageview || isLoadingStats;
+
+ return (
+
+
+
+ {info?.name}
+
+
+
{actions}
+
+
+
+
{stats && }
+
+
+
+
+ useGlobalStateStore.setState({
+ showPreviousPeriod: checked,
+ })
+ }
+ />
+ {t('Previous period')}
+
+
+
}
+ onClick={handleRefresh}
+ />
+
+ {showDateFilter && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+});
+TelemetryOverview.displayName = 'TelemetryOverview';
+
+const MetricsBar: React.FC<{
+ stats: AppRouterOutput['telemetry']['stats'];
+}> = React.memo((props) => {
+ const { t } = useTranslation();
+ const { pageviews, uniques } = props.stats || {};
+
+ return (
+
+
+
+
+ );
+});
+MetricsBar.displayName = 'MetricsBar';
diff --git a/src/client/components/website/WebsiteOverview.tsx b/src/client/components/website/WebsiteOverview.tsx
index 09e696e..8ae3678 100644
--- a/src/client/components/website/WebsiteOverview.tsx
+++ b/src/client/components/website/WebsiteOverview.tsx
@@ -5,7 +5,7 @@ import { DateFilter } from '../DateFilter';
import { WebsiteInfo } from '../../api/model/website';
import { getDateArray } from '../../utils/date';
import { useEvent } from '../../hooks/useEvent';
-import { MetricCard } from './MetricCard';
+import { MetricCard } from '../MetricCard';
import { formatNumber, formatShortTime } from '../../utils/common';
import { WebsiteOnlineCount } from './WebsiteOnlineCount';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
@@ -151,7 +151,7 @@ export const WebsiteOverview: React.FC<{
});
WebsiteOverview.displayName = 'WebsiteOverview';
-export const MetricsBar: React.FC<{
+const MetricsBar: React.FC<{
stats: AppRouterOutput['website']['stats'];
}> = React.memo((props) => {
const { t } = useTranslation();
diff --git a/src/client/pages/Telemetry/Detail.tsx b/src/client/pages/Telemetry/Detail.tsx
new file mode 100644
index 0000000..5ef6b4e
--- /dev/null
+++ b/src/client/pages/Telemetry/Detail.tsx
@@ -0,0 +1,30 @@
+import { Card } from 'antd';
+import React from 'react';
+import { useParams } from 'react-router';
+import { NotFoundTip } from '../../components/NotFoundTip';
+import { useCurrentWorkspaceId } from '../../store/user';
+import { TelemetryOverview } from '../../components/telemetry/TelemetryOverview';
+
+export const TelemetryDetailPage: React.FC = React.memo(() => {
+ const { telemetryId } = useParams();
+ const workspaceId = useCurrentWorkspaceId();
+
+ if (!telemetryId) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+});
+TelemetryDetailPage.displayName = 'TelemetryDetailPage';
diff --git a/src/client/pages/Telemetry/index.tsx b/src/client/pages/Telemetry/index.tsx
index 76b7c71..5f7c2f9 100644
--- a/src/client/pages/Telemetry/index.tsx
+++ b/src/client/pages/Telemetry/index.tsx
@@ -1,7 +1,14 @@
import React from 'react';
import { TelemetryList } from '../../components/telemetry/TelemetryList';
+import { Route, Routes } from 'react-router-dom';
+import { TelemetryDetailPage } from './Detail';
export const TelemetryPage: React.FC = React.memo(() => {
- return ;
+ return (
+
+ } />
+ } />
+
+ );
});
TelemetryPage.displayName = 'TelemetryPage';
diff --git a/src/server/model/_schema/filter.ts b/src/server/model/_schema/filter.ts
index 2cce966..c44c7f4 100644
--- a/src/server/model/_schema/filter.ts
+++ b/src/server/model/_schema/filter.ts
@@ -5,11 +5,11 @@ export const baseFilterSchema = z.object({
country: z.string(),
region: z.string(),
city: z.string(),
+ timezone: z.string(),
});
export const websiteFilterSchema = baseFilterSchema.merge(
z.object({
- timezone: z.string(),
referrer: z.string(),
title: z.string(),
os: z.string(),
@@ -18,14 +18,19 @@ export const websiteFilterSchema = baseFilterSchema.merge(
})
);
-const websiteStatsItemType = z.object({
+export const statsItemType = z.object({
value: z.number(),
prev: z.number(),
});
-export const websiteStatsSchema = z.object({
- bounces: websiteStatsItemType,
- pageviews: websiteStatsItemType,
- totaltime: websiteStatsItemType,
- uniques: websiteStatsItemType,
+export const baseStatsSchema = z.object({
+ pageviews: statsItemType,
+ uniques: statsItemType,
});
+
+export const websiteStatsSchema = baseStatsSchema.merge(
+ z.object({
+ totaltime: statsItemType,
+ bounces: statsItemType,
+ })
+);
diff --git a/src/server/model/telemetry.ts b/src/server/model/telemetry.ts
index 091ff5c..df1c1bc 100644
--- a/src/server/model/telemetry.ts
+++ b/src/server/model/telemetry.ts
@@ -231,7 +231,8 @@ export async function getTelemetryStats(
from (
select
"TelemetryEvent"."sessionId",
- ${getDateQuery('"TelemetryEvent"."createdAt"', 'hour')}
+ ${getDateQuery('"TelemetryEvent"."createdAt"', 'hour')},
+ count(*) as c
from "TelemetryEvent"
join "Telemetry"
on "TelemetryEvent"."telemetryId" = "Telemetry"."id"
diff --git a/src/server/trpc/routers/telemetry.ts b/src/server/trpc/routers/telemetry.ts
index ed4e16d..dc05e44 100644
--- a/src/server/trpc/routers/telemetry.ts
+++ b/src/server/trpc/routers/telemetry.ts
@@ -14,14 +14,20 @@ import {
import { prisma } from '../../model/_client';
import { TelemetryModelSchema } from '../../prisma/zod';
import { OpenApiMeta } from 'trpc-openapi';
-import { baseFilterSchema } from '../../model/_schema/filter';
+import {
+ baseFilterSchema,
+ baseStatsSchema,
+ statsItemType,
+} from '../../model/_schema/filter';
import {
getTelemetryPageview,
getTelemetryPageviewMetrics,
getTelemetrySession,
getTelemetrySessionMetrics,
+ getTelemetryStats,
} from '../../model/telemetry';
import { BaseQueryFilters } from '../../utils/prisma';
+import dayjs from 'dayjs';
export const telemetryRouter = router({
all: workspaceProcedure
@@ -44,6 +50,31 @@ export const telemetryRouter = router({
},
});
+ return res;
+ }),
+ info: workspaceProcedure
+ .meta(
+ buildTelemetryOpenapi({
+ method: 'GET',
+ path: '/info',
+ })
+ )
+ .input(
+ z.object({
+ telemetryId: z.string(),
+ })
+ )
+ .output(TelemetryModelSchema.nullable())
+ .query(async ({ input }) => {
+ const { workspaceId, telemetryId } = input;
+
+ const res = await prisma.telemetry.findUnique({
+ where: {
+ workspaceId,
+ id: telemetryId,
+ },
+ });
+
return res;
}),
eventCount: workspaceProcedure
@@ -257,6 +288,85 @@ export const telemetryRouter = router({
return [];
}),
+ stats: workspaceProcedure
+ .meta(
+ buildTelemetryOpenapi({
+ method: 'GET',
+ path: '/stats',
+ })
+ )
+ .input(
+ z
+ .object({
+ telemetryId: z.string(),
+ startAt: z.number(),
+ endAt: z.number(),
+ unit: z.string().optional(),
+ })
+ .merge(baseFilterSchema.partial())
+ )
+ .output(baseStatsSchema)
+ .query(async ({ input }) => {
+ const {
+ telemetryId,
+ timezone,
+ url,
+ country,
+ region,
+ city,
+ startAt,
+ endAt,
+ } = input;
+
+ const startDate = new Date(startAt);
+ const endDate = new Date(endAt);
+ // const { startDate, endDate, unit } = await parseDateRange({
+ // telemetryId,
+ // 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: input.unit,
+ url,
+ country,
+ region,
+ city,
+ } as BaseQueryFilters;
+
+ const [metrics, prevPeriod] = await Promise.all([
+ getTelemetryStats(telemetryId, {
+ ...filters,
+ startDate,
+ endDate,
+ }),
+ getTelemetryStats(telemetryId, {
+ ...filters,
+ startDate: prevStartDate,
+ endDate: prevEndDate,
+ }),
+ ]);
+
+ const stats = Object.keys(metrics[0]).reduce((obj, key) => {
+ const current = Number(metrics[0][key]) || 0;
+ const prev = Number(prevPeriod[0][key]) || 0;
+ obj[key] = {
+ value: current,
+ prev,
+ };
+ return obj;
+ }, {} as Record);
+
+ return baseStatsSchema.parse(stats);
+ }),
});
function buildTelemetryOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {