From 5910f62580c9483d252099739718004e25767663 Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Sat, 14 Mar 2026 01:43:03 +0700 Subject: [PATCH 1/2] feat: batch destination metrics requests in portal list view Collapse N per-row metrics API calls into a single batched request using dimensions[]=destination_id grouping. Co-Authored-By: Claude Opus 4.6 --- .../src/common/MetricsChart/useMetrics.ts | 82 +++++++++++++++++++ .../DestinationEventsCell.tsx | 20 ++--- .../DestinationsList/DestinationList.tsx | 22 ++++- 3 files changed, 109 insertions(+), 15 deletions(-) diff --git a/internal/portal/src/common/MetricsChart/useMetrics.ts b/internal/portal/src/common/MetricsChart/useMetrics.ts index 80361549..c0c444a7 100644 --- a/internal/portal/src/common/MetricsChart/useMetrics.ts +++ b/internal/portal/src/common/MetricsChart/useMetrics.ts @@ -137,3 +137,85 @@ export function useMetrics({ return { data, error, isLoading }; } + +export function useBatchedMetrics({ + measures, + destinationIds, + timeframe, + filters, + granularity: granularityOverride, +}: { + measures: string[]; + destinationIds: string[]; + timeframe: Timeframe; + filters?: Record; + granularity?: string; +}) { + const apiClient = useContext(ApiContext); + + const measuresKey = measures.join(","); + const filtersKey = filters + ? Object.entries(filters) + .map(([k, v]) => `${k}=${v}`) + .join(",") + : ""; + const idsKey = [...destinationIds].sort().join(","); + + const url = useMemo(() => { + if (destinationIds.length === 0) return null; + + const { start, end } = getDateRange(timeframe); + const params = new URLSearchParams(); + params.set("time[start]", start); + params.set("time[end]", end); + + for (const m of measures) { + params.append("measures[]", m); + } + + const sortedIds = [...destinationIds].sort(); + for (const id of sortedIds) { + params.append("filters[destination_id][]", id); + } + + params.append("dimensions[]", "destination_id"); + + if (filters) { + for (const [k, v] of Object.entries(filters)) { + params.set(`filters[${k}]`, v); + } + } + + params.set( + "granularity", + granularityOverride ?? getGranularity(timeframe), + ); + + return `metrics/attempts?${params.toString()}`; + }, [idsKey, measuresKey, filtersKey, granularityOverride, timeframe]); + + const { data, error, isLoading } = useSWR( + url, + (path: string) => apiClient.fetchRoot(path), + { + refreshInterval: 60_000, + revalidateOnFocus: false, + }, + ); + + const grouped = useMemo(() => { + if (!data) return undefined; + + const result: Record = {}; + for (const point of data.data) { + const destId = point.dimensions.destination_id; + if (!result[destId]) { + result[destId] = []; + } + result[destId].push(point); + } + return result; + }, [data]); + + return { data: grouped, error, isLoading }; +} diff --git a/internal/portal/src/scenes/DestinationsList/DestinationEventsCell.tsx b/internal/portal/src/scenes/DestinationsList/DestinationEventsCell.tsx index f00a88c2..e0b57936 100644 --- a/internal/portal/src/scenes/DestinationsList/DestinationEventsCell.tsx +++ b/internal/portal/src/scenes/DestinationsList/DestinationEventsCell.tsx @@ -1,8 +1,9 @@ import Sparkline from "../../common/Sparkline/Sparkline"; -import { useMetrics } from "../../common/MetricsChart/useMetrics"; +import { MetricsDataPoint } from "../../common/MetricsChart/useMetrics"; interface DestinationEventsCellProps { - destinationId: string; + metricsData?: MetricsDataPoint[]; + isLoading: boolean; } function formatCount(n: number): string { @@ -12,21 +13,14 @@ function formatCount(n: number): string { } const DestinationEventsCell: React.FC = ({ - destinationId, + metricsData, + isLoading, }) => { - const { data, isLoading } = useMetrics({ - measures: ["successful_count", "failed_count"], - destinationId, - timeframe: "24h", - granularity: "4h", - filters: { attempt_number: "0", manual: "false" }, - }); - - if (isLoading || !data) { + if (isLoading || !metricsData) { return ; } - const points = data.data.map((d) => ({ + const points = metricsData.map((d) => ({ successful: d.metrics.successful_count ?? 0, failed: d.metrics.failed_count ?? 0, })); diff --git a/internal/portal/src/scenes/DestinationsList/DestinationList.tsx b/internal/portal/src/scenes/DestinationsList/DestinationList.tsx index af565db7..af9d7544 100644 --- a/internal/portal/src/scenes/DestinationsList/DestinationList.tsx +++ b/internal/portal/src/scenes/DestinationsList/DestinationList.tsx @@ -1,6 +1,6 @@ import "./DestinationList.scss"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import useSWR from "swr"; import Badge from "../../common/Badge/Badge"; @@ -8,6 +8,7 @@ import Button from "../../common/Button/Button"; import { Checkbox } from "../../common/Checkbox/Checkbox"; import Dropdown from "../../common/Dropdown/Dropdown"; import { AddIcon, FilterIcon, Loading } from "../../common/Icons"; +import { useBatchedMetrics } from "../../common/MetricsChart/useMetrics"; import SearchInput from "../../common/SearchInput/SearchInput"; import Table from "../../common/Table/Table"; import Tooltip from "../../common/Tooltip/Tooltip"; @@ -20,6 +21,20 @@ import DestinationEventsCell from "./DestinationEventsCell"; const DestinationList: React.FC = () => { const { data: destinations } = useSWR("destinations"); const destination_types = useDestinationTypes(); + + const destinationIds = useMemo( + () => destinations?.map((d) => d.id) ?? [], + [destinations], + ); + const { data: batchedMetrics, isLoading: metricsLoading } = + useBatchedMetrics({ + measures: ["successful_count", "failed_count"], + destinationIds, + timeframe: "24h", + granularity: "4h", + filters: { attempt_number: "0", manual: "false" }, + }); + const [searchTerm, setSearchTerm] = useState(""); const [selectedStatus, setSelectedStatus] = useState>( {}, @@ -122,7 +137,10 @@ const DestinationList: React.FC = () => { ) : ( ), - , + , ].filter((entry) => entry !== null), link: `/destinations/${destination.id}`, })) || []; From a97be0cc4cb799ed8a25f15026171d81c5b4fd56 Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Sat, 14 Mar 2026 02:20:05 +0700 Subject: [PATCH 2/2] fix: update attempt_number filter to 1-based indexing in portal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Missed in #740 — portal still filtered for attempt_number=0. Co-Authored-By: Claude Opus 4.6 --- internal/portal/src/scenes/Destination/DestinationMetrics.tsx | 2 +- internal/portal/src/scenes/DestinationsList/DestinationList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/portal/src/scenes/Destination/DestinationMetrics.tsx b/internal/portal/src/scenes/Destination/DestinationMetrics.tsx index 22a1f9d2..30de86a5 100644 --- a/internal/portal/src/scenes/Destination/DestinationMetrics.tsx +++ b/internal/portal/src/scenes/Destination/DestinationMetrics.tsx @@ -61,7 +61,7 @@ const DestinationMetrics: React.FC = ({ measures: ["count"], destinationId: destination.id, timeframe, - filters: { attempt_number: "0", manual: "false" }, + filters: { attempt_number: "1", manual: "false" }, }); const delivery = useMetrics({ diff --git a/internal/portal/src/scenes/DestinationsList/DestinationList.tsx b/internal/portal/src/scenes/DestinationsList/DestinationList.tsx index af9d7544..6bfcd611 100644 --- a/internal/portal/src/scenes/DestinationsList/DestinationList.tsx +++ b/internal/portal/src/scenes/DestinationsList/DestinationList.tsx @@ -32,7 +32,7 @@ const DestinationList: React.FC = () => { destinationIds, timeframe: "24h", granularity: "4h", - filters: { attempt_number: "0", manual: "false" }, + filters: { attempt_number: "1", manual: "false" }, }); const [searchTerm, setSearchTerm] = useState("");