Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ export interface TonApiStats {
requests?: number;
liteproxyRequests?: number;
liteproxyConnections?: number;
webhooksDelivered?: number;
webhooksFailed?: number;
}[];
}
85 changes: 85 additions & 0 deletions src/features/tonapi/statistics/model/ton-api-stats.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export class TonApiStatsStore {

liteproxyStats$ = new Loadable<TonApiStats | null>(null);

webhooksStats$ = new Loadable<TonApiStats | null>(null);

constructor() {
makeAutoObservable(this);

Expand All @@ -25,6 +27,7 @@ export class TonApiStatsStore {
if (project) {
this.fetchStats();
this.fetchLiteproxyStats();
this.fetchWebhooksStats();
}
}
);
Expand Down Expand Up @@ -76,9 +79,40 @@ export class TonApiStatsStore {
);
});

fetchWebhooksStats = this.webhooksStats$.createAsyncAction(async () => {
const weekAgo = Math.round(Date.now() / 1000 - 3600 * 24 * 7);
const end = Math.floor(Date.now() / 1000);
const halfAnHourPeriod = 60 * 30;

// Fetch both webhook_delivered and webhook_failed metrics
const deliveredResponse = await apiClient.api.getProjectTonApiStats({
project_id: projectsStore.selectedProject!.id,
start: weekAgo,
step: halfAnHourPeriod,
end,
detailed: false,
dashboard: DTOGetProjectTonApiStatsParamsDashboardEnum.DTOTonapiWebhook
});

const failedResponse = await apiClient.api.getProjectTonApiStats({
project_id: projectsStore.selectedProject!.id,
start: weekAgo,
step: halfAnHourPeriod,
end,
detailed: false,
dashboard: DTOGetProjectTonApiStatsParamsDashboardEnum.DTOTonapiWebhook
});

return mapWebhooksStatsDTOToTonApiStats(
deliveredResponse.data.stats,
failedResponse.data.stats
);
});

clearStore(): void {
this.stats$.clear();
this.liteproxyStats$.clear();
this.webhooksStats$.clear();
}
}

Expand Down Expand Up @@ -137,3 +171,54 @@ function mapLiteproxyStatsDTOToTonApiStats(

return chart.length > 0 ? { chart } : null;
}

function mapWebhooksStatsDTOToTonApiStats(
deliveredStats: DTOStats,
failedStats: DTOStats
): TonApiStats | null {
// Create a map with timestamps as keys
const timelineMap = new Map<
number,
{ time: number; webhooksDelivered?: number; webhooksFailed?: number }
>();

// Process delivered stats - filter by operation type "delivered"
if (deliveredStats.result.length > 0) {
deliveredStats.result.forEach(item => {
if (item.metric?.operation === 'delivered') {
item.values.forEach(([timestamp, value]) => {
const time = Number(timestamp) * 1000;

if (!timelineMap.has(time)) {
timelineMap.set(time, { time });
}

const entry = timelineMap.get(time)!;
entry.webhooksDelivered = Math.round(Number(value) * 100) / 100;
});
}
});
}

// Process failed stats - filter by operation type "failed"
if (failedStats.result.length > 0) {
failedStats.result.forEach(item => {
if (item.metric?.operation === 'failed') {
item.values.forEach(([timestamp, value]) => {
const time = Number(timestamp) * 1000;

if (!timelineMap.has(time)) {
timelineMap.set(time, { time });
}

const entry = timelineMap.get(time)!;
entry.webhooksFailed = Math.round(Number(value) * 100) / 100;
});
}
});
}

const chart = Array.from(timelineMap.values()).sort((a, b) => a.time - b.time);

return chart.length > 0 ? { chart } : null;
}
162 changes: 162 additions & 0 deletions src/features/tonapi/statistics/ui/WebhooksStatsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
Text,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Flex,
Center,
Spinner,
Box
} from '@chakra-ui/react';
import { FC } from 'react';
import { XAxis, YAxis, Tooltip, ResponsiveContainer, LineChart, Line } from 'recharts';
import { TonApiStats } from '../model/interfaces';
import { observer } from 'mobx-react-lite';
import { tonApiStatsStore } from 'src/shared/stores';

interface WebhooksStatsModalProps {
isOpen: boolean;
onClose: () => void;
}

const dateFormatter = (time: number) => {
const date = new Date(time);
return `${date.toLocaleDateString()} ${date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}`;
};

const getWebhooksDeliveredData = (
data: TonApiStats
): Array<{ time: number; value: number | undefined }> => {
return data.chart
.filter(item => item.webhooksDelivered !== undefined)
.map(item => ({
time: item.time,
value: item.webhooksDelivered
}));
};

const getWebhooksFailedData = (
data: TonApiStats
): Array<{ time: number; value: number | undefined }> => {
return data.chart
.filter(item => item.webhooksFailed !== undefined)
.map(item => ({
time: item.time,
value: item.webhooksFailed
}));
};

const WebhooksStatsModal: FC<WebhooksStatsModalProps> = observer(({ isOpen, onClose }) => {
const { isResolved, value } = tonApiStatsStore.webhooksStats$;

if (!isResolved) {
return (
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Webhooks Statistics</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Center h="400px">
<Spinner />
</Center>
</ModalBody>
</ModalContent>
</Modal>
);
}

if (!value) {
return (
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Webhooks Statistics</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Center h="400px">
<Text color="text.secondary">No data available</Text>
</Center>
</ModalBody>
</ModalContent>
</Modal>
);
}

return (
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Webhooks Statistics</ModalHeader>
<ModalCloseButton />
<ModalBody pb={4}>
<Flex direction="column" gap={8}>
<Text textStyle="text.label1" mb={1} color="text.secondary" fontSize={16}>
Delivered Webhooks
</Text>
<Box h={250}>
<ResponsiveContainer height="100%">
<LineChart
data={getWebhooksDeliveredData(value)}
margin={{ left: 0, right: 0 }}
>
<XAxis
dataKey="time"
type="number"
domain={['dataMin', 'dataMax']}
tickFormatter={dateFormatter}
/>
<YAxis />
<Tooltip labelFormatter={time => dateFormatter(Number(time))} />
<Line
type="monotone"
dataKey="value"
stroke="#2E84E5"
dot={false}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
</Box>

<Text textStyle="text.label1" mb={1} color="text.secondary" fontSize={16}>
Failed Webhooks
</Text>
<Box h={250}>
<ResponsiveContainer height="100%">
<LineChart
data={getWebhooksFailedData(value)}
margin={{ left: 0, right: 0 }}
>
<XAxis
dataKey="time"
type="number"
domain={['dataMin', 'dataMax']}
tickFormatter={dateFormatter}
/>
<YAxis />
<Tooltip labelFormatter={time => dateFormatter(Number(time))} />
<Line
type="monotone"
dataKey="value"
stroke="#E55252"
dot={false}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
</Box>
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
});

export default WebhooksStatsModal;
1 change: 1 addition & 0 deletions src/features/tonapi/statistics/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { DashboardTierCard } from './DashboardTierCard';
export { default as DashboardChart } from './DashboardChart';
export { default as LiteproxyStatsModal } from './LiteproxyStatsModal';
export { default as WebhooksStatsModal } from './WebhooksStatsModal';
33 changes: 1 addition & 32 deletions src/features/tonapi/webhooks/model/webhooks.store.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
apiClient,
createImmediateReaction,
DTOGetProjectTonApiStatsParamsDashboardEnum,
Loadable,
Network
} from 'src/shared';
import { createImmediateReaction, Loadable, Network } from 'src/shared';
import { Webhook, CreateWebhookForm } from './interfaces';
import { projectsStore } from 'src/shared/stores';
import { makeAutoObservable } from 'mobx';
Expand All @@ -14,15 +8,12 @@ import {
RTWebhookListStatusEnum
} from 'src/shared/api/streaming-api';
import { Address } from '@ton/core';
import { WebhooksStat } from './interfaces/webhooks';

export type Subscription = RTWebhookAccountTxSubscriptions['account_tx_subscriptions'][0];

class WebhooksStore {
webhooks$ = new Loadable<Webhook[]>([]);

stats$ = new Loadable<WebhooksStat | null>(null);

selectedWebhook: Webhook | null = null;

subscriptions$ = new Loadable<Subscription[]>([]);
Expand All @@ -45,7 +36,6 @@ class WebhooksStore {

if (project) {
this.fetchWebhooks();
this.fetchWebhooksStats();
}
}
);
Expand Down Expand Up @@ -132,26 +122,6 @@ class WebhooksStore {
return response;
});

fetchWebhooksStats = this.stats$.createAsyncAction(async () => {
const now = new Date();
const startOfMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));

const startTimestamp = Math.floor(startOfMonth.getTime() / 1000);
const endTimestamp = Math.floor(now.getTime() / 1000);

const response = await apiClient.api
.getProjectTonApiStats({
project_id: projectsStore.selectedProject!.id,
start: startTimestamp,
end: endTimestamp,
step: 3600,
dashboard: DTOGetProjectTonApiStatsParamsDashboardEnum.DTOTonapiWebhook
})
.then(res => res.data.stats);

return response;
});

createWebhook = this.webhooks$.createAsyncAction(
async ({ endpoint }: CreateWebhookForm) => {
const resCreateWebhook = await rtTonApiClient.webhooks
Expand Down Expand Up @@ -313,7 +283,6 @@ class WebhooksStore {

clearStore(): void {
this.webhooks$.clear();
this.stats$.clear();
this.selectedWebhook = null;
this.subscriptions$.clear();
this.subscriptionsPage = 1;
Expand Down
Loading