diff --git a/studio/components/interfaces/Reports/Reports.constants.ts b/studio/components/interfaces/Reports/Reports.constants.ts index 914c8a068ae..91cd9cd0870 100644 --- a/studio/components/interfaces/Reports/Reports.constants.ts +++ b/studio/components/interfaces/Reports/Reports.constants.ts @@ -75,8 +75,8 @@ export const PRESET_CONFIG: Record = { timestamp ASC`, }, topRoutes: { - queryType: "logs", - sql: (filters)=> ` + queryType: 'logs', + sql: (filters) => ` select request.path as path, request.method as method, @@ -95,7 +95,7 @@ export const PRESET_CONFIG: Record = { count desc limit 3 - ` + `, }, errorCounts: { queryType: 'logs', @@ -118,8 +118,8 @@ export const PRESET_CONFIG: Record = { `, }, topErrorRoutes: { - queryType: "logs", - sql: (filters)=> ` + queryType: 'logs', + sql: (filters) => ` select request.path as path, request.method as method, @@ -140,7 +140,7 @@ export const PRESET_CONFIG: Record = { count desc limit 3 - ` + `, }, responseSpeed: { queryType: 'logs', @@ -162,8 +162,8 @@ export const PRESET_CONFIG: Record = { `, }, topSlowRoutes: { - queryType: "logs", - sql: (filters)=> ` + queryType: 'logs', + sql: (filters) => ` select request.path as path, request.method as method, @@ -183,7 +183,44 @@ export const PRESET_CONFIG: Record = { avg desc limit 3 - ` + `, + }, + networkTraffic: { + queryType: 'logs', + sql: (filters) => ` + select + cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp, + coalesce( + safe_divide( + sum( + cast(coalesce(headers.content_length, "0") as int64) + ), + 1000000 + ), + 0 + ) as ingress_mb, + coalesce( + safe_divide( + sum( + cast(coalesce(resp_headers.content_length, "0") as int64) + ), + 1000000 + ), + 0 + ) as egress_mb, + FROM + edge_logs t + cross join unnest(metadata) as m + cross join unnest(m.response) as response + cross join unnest(m.request) as request + cross join unnest(request.headers) as headers + cross join unnest(response.headers) as resp_headers + ${generateRexepWhere(filters)} + GROUP BY + timestamp + ORDER BY + timestamp ASC + `, }, }, }, diff --git a/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx b/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx index d6b97f6c3ab..9ecdf49b13c 100644 --- a/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx +++ b/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx @@ -9,7 +9,59 @@ import { Button, Collapsible, IconChevronRight } from 'ui' import { queryParamsToObject } from '../Reports.utils' import { Fragment } from 'react' import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted' +import sumBy from 'lodash/sumBy' +export const networkTrafficRenderer = ( + props: ReportWidgetProps<{ + timestamp: string + ingress: number + egress: number + }> +) => { + const data = useFillTimeseriesSorted( + props.data, + 'timestamp', + ['ingress_mb', 'egress_mb'], + 0, + props.params?.iso_timestamp_start, + props.params?.iso_timestamp_end + ) + const totalIngress = sumBy(props.data, 'ingress_mb') + const totalEgress = sumBy(props.data, 'egress_mb') + + function determinePrecision(valueInMb: number) { + return valueInMb < 0.001 ? 7 : totalIngress > 1 ? 2 : 4 + } + return ( +
+ + + +
+ ) +} export const TotalRequestsChartRenderer = ( props: ReportWidgetProps<{ timestamp: string diff --git a/studio/components/ui/Charts/AreaChart.tsx b/studio/components/ui/Charts/AreaChart.tsx index f83fcf91625..256fef27935 100644 --- a/studio/components/ui/Charts/AreaChart.tsx +++ b/studio/components/ui/Charts/AreaChart.tsx @@ -24,7 +24,7 @@ export interface AreaChartProps extends CommonChartProps { displayDateInUtc?: boolean } -const AreaChart: React.FC = ({ +const AreaChart = ({ data, yAxisKey, xAxisKey, @@ -36,8 +36,9 @@ const AreaChart: React.FC = ({ displayDateInUtc, minimalHeader, className = '', + valuePrecision, size = 'normal', -}) => { +}: AreaChartProps) => { const { Container } = useChartSize(size) const [focusDataIndex, setFocusDataIndex] = useState(null) @@ -62,7 +63,7 @@ const AreaChart: React.FC = ({ customDateFormat={customDateFormat} highlightedValue={ typeof resolvedHighlightedValue === 'number' - ? numberFormatter(resolvedHighlightedValue) + ? numberFormatter(resolvedHighlightedValue, valuePrecision) : resolvedHighlightedValue } highlightedLabel={resolvedHighlightedLabel} diff --git a/studio/components/ui/Charts/BarChart.tsx b/studio/components/ui/Charts/BarChart.tsx index 2da9cf9a76e..cba8bf685e2 100644 --- a/studio/components/ui/Charts/BarChart.tsx +++ b/studio/components/ui/Charts/BarChart.tsx @@ -13,13 +13,12 @@ dayjs.extend(utc) export interface BarChartProps extends CommonChartProps { yAxisKey: string xAxisKey: string - format?: string customDateFormat?: string displayDateInUtc?: boolean onBarClick?: (datum: Datum, tooltipData?: Parameters[0]) => void } -const BarChart: React.FC = ({ +const BarChart = ({ data, yAxisKey, xAxisKey, @@ -30,10 +29,11 @@ const BarChart: React.FC = ({ highlightedLabel, displayDateInUtc, minimalHeader, + valuePrecision, className = '', size = 'normal', onBarClick, -}) => { +}: BarChartProps) => { const { Container } = useChartSize(size) const [focusDataIndex, setFocusDataIndex] = useState(null) @@ -58,7 +58,7 @@ const BarChart: React.FC = ({ customDateFormat={customDateFormat} highlightedValue={ typeof resolvedHighlightedValue === 'number' - ? numberFormatter(resolvedHighlightedValue) + ? numberFormatter(resolvedHighlightedValue, valuePrecision) : resolvedHighlightedValue } highlightedLabel={resolvedHighlightedLabel} diff --git a/studio/components/ui/Charts/ChartHeader.tsx b/studio/components/ui/Charts/ChartHeader.tsx index ee515259815..93ce980b751 100644 --- a/studio/components/ui/Charts/ChartHeader.tsx +++ b/studio/components/ui/Charts/ChartHeader.tsx @@ -1,8 +1,6 @@ -import { DateTimeFormats } from './Charts.constants' - export interface ChartHeaderProps { title?: string - format?: string + format?: string | ((value: unknown) => string) customDateFormat?: string minimalHeader?: boolean displayDateInUtc?: boolean @@ -15,7 +13,7 @@ const ChartHeader: React.FC = ({ highlightedLabel, title, minimalHeader = false, -}) => { +}: ChartHeaderProps) => { const chartTitle = (

{title}

) @@ -24,8 +22,10 @@ const ChartHeader: React.FC = ({
- {highlightedValue} - {format} + {highlightedValue !== undefined && String(highlightedValue)} + + {typeof format === 'function' ? format(highlightedValue) : format} +
) const label =
{highlightedLabel}
@@ -35,7 +35,7 @@ const ChartHeader: React.FC = ({
{title && chartTitle}
- {highlightedValue && highlighted} + {highlightedValue !== undefined && highlighted} {label}
@@ -45,7 +45,7 @@ const ChartHeader: React.FC = ({ return (
{title && chartTitle} - {highlightedValue && highlighted} + {highlightedValue !== undefined && highlighted} {label}
) diff --git a/studio/components/ui/Charts/Charts.types.tsx b/studio/components/ui/Charts/Charts.types.tsx index e02178f9350..bf0d1c0aa97 100644 --- a/studio/components/ui/Charts/Charts.types.tsx +++ b/studio/components/ui/Charts/Charts.types.tsx @@ -13,6 +13,7 @@ export interface CommonChartProps > { title?: string className?: string + valuePrecision?: number size?: 'tiny' | 'small' | 'normal' | 'large' } @@ -27,7 +28,7 @@ export interface StackedChartProps extends CommonChartProps { export type HeaderType = { attribute: string focus: number | null - format?: string + format?: string | ((value: unknown) => string); highlightedValue?: number | string highlightedLabel?: string data: D[] diff --git a/studio/pages/project/[ref]/reports/api-overview.tsx b/studio/pages/project/[ref]/reports/api-overview.tsx index 2bf633a05b1..439d413d963 100644 --- a/studio/pages/project/[ref]/reports/api-overview.tsx +++ b/studio/pages/project/[ref]/reports/api-overview.tsx @@ -13,6 +13,7 @@ import { ErrorCountsChartRenderer, ResponseSpeedChartRenderer, TopApiRoutesRenderer, + networkTrafficRenderer, } from 'components/interfaces/Reports/renderers/ApiRenderers' import { useState, useEffect } from 'react' import ReportHeader from 'components/interfaces/Reports/ReportHeader' @@ -88,6 +89,15 @@ export const ApiReport: NextPageWithLayout = () => { appendProps={{ data: report.data.topSlowRoutes || [] }} append={TopApiRoutesRenderer} /> + + ) } @@ -106,6 +116,7 @@ const useApiReport = () => { const topErrorRoutes = queryHooks.topErrorRoutes() const responseSpeed = queryHooks.responseSpeed() const topSlowRoutes = queryHooks.topSlowRoutes() + const networkTraffic = queryHooks.networkTraffic() const activeHooks = [ totalRequests, topRoutes, @@ -113,6 +124,7 @@ const useApiReport = () => { topErrorRoutes, responseSpeed, topSlowRoutes, + networkTraffic, ] const [filters, setFilters] = useState([]) const addFilter = (filter: ReportFilterItem) => { @@ -161,17 +173,21 @@ const useApiReport = () => { if (topSlowRoutes.changeQuery) { topSlowRoutes.changeQuery(PRESET_CONFIG.api.queries.topSlowRoutes.sql(filters)) } + + if (networkTraffic.changeQuery) { + networkTraffic.changeQuery(PRESET_CONFIG.api.queries.networkTraffic.sql(filters)) + } }, [JSON.stringify(filters)]) const handleRefresh = async () => { - activeHooks.forEach(hook => hook.runQuery()) + activeHooks.forEach((hook) => hook.runQuery()) } const handleSetParams = (params: Partial) => { - activeHooks.forEach(hook => { + activeHooks.forEach((hook) => { hook.setParams?.((prev: LogsEndpointParams) => ({ ...prev, ...params })) }) } - const isLoading = activeHooks.some(hook => hook.isLoading) + const isLoading = activeHooks.some((hook) => hook.isLoading) return { data: { totalRequests: totalRequests.logData, @@ -180,6 +196,7 @@ const useApiReport = () => { topRoutes: topRoutes.logData, topErrorRoutes: topErrorRoutes.logData, topSlowRoutes: topSlowRoutes.logData, + networkTraffic: networkTraffic.logData, }, params: { totalRequests: totalRequests.params, @@ -188,6 +205,7 @@ const useApiReport = () => { topRoutes: topRoutes.params, topErrorRoutes: topErrorRoutes.params, topSlowRoutes: topSlowRoutes.params, + networkTraffic: networkTraffic.params, }, mergeParams: handleSetParams, filters, diff --git a/studio/tests/pages/projects/reports/api-report.test.js b/studio/tests/pages/projects/reports/api-report.test.js index be214ef6b73..94e889cc389 100644 --- a/studio/tests/pages/projects/reports/api-report.test.js +++ b/studio/tests/pages/projects/reports/api-report.test.js @@ -15,6 +15,8 @@ test(`static elements`, async () => { await screen.findByText('Total Requests') await screen.findByText('Response Errors') await screen.findByText('Response Speed') + await screen.findByText('Network Ingress') + await screen.findByText('Network Egress') await screen.findByText(/Last 24 hours/) await screen.findByText(/Custom/) })