mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
416 lines
12 KiB
TypeScript
416 lines
12 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { get } from 'data/fetchers'
|
|
import { AnalyticsInterval } from 'data/analytics/constants'
|
|
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
|
|
import { getHttpStatusCodeInfo } from 'lib/http-status-codes'
|
|
import { analyticsIntervalToGranularity, useEdgeFnIdToName } from './report.utils'
|
|
import { REPORT_STATUS_CODE_COLORS } from './report.utils'
|
|
|
|
/**
|
|
* METRICS
|
|
* Each chart in the UI has a corresponding metric key.
|
|
*/
|
|
|
|
const METRIC_KEYS = [
|
|
'TotalInvocations',
|
|
'ExecutionStatusCodes',
|
|
'InvocationsByRegion',
|
|
'ExecutionTime',
|
|
]
|
|
|
|
type MetricKey = (typeof METRIC_KEYS)[number]
|
|
|
|
/**
|
|
* SQL
|
|
* Each metric has a corresponding SQL query.
|
|
*/
|
|
|
|
const METRIC_SQL: Record<
|
|
MetricKey,
|
|
(interval: AnalyticsInterval, functionIds?: string[]) => string
|
|
> = {
|
|
TotalInvocations: (interval, functionIds) => {
|
|
return `
|
|
--edgefn-report-invocations
|
|
select
|
|
timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp,
|
|
function_id,
|
|
count(*) as count
|
|
from
|
|
function_edge_logs
|
|
CROSS JOIN UNNEST(metadata) AS m
|
|
CROSS JOIN UNNEST(m.request) AS request
|
|
CROSS JOIN UNNEST(m.response) AS response
|
|
${
|
|
functionIds && functionIds.length > 0
|
|
? `WHERE function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})`
|
|
: ''
|
|
}
|
|
group by
|
|
timestamp,
|
|
function_id
|
|
order by
|
|
timestamp desc;
|
|
|
|
`
|
|
},
|
|
ExecutionStatusCodes: (interval, functionIds) => {
|
|
return `
|
|
--edgefn-report-execution-status-codes
|
|
select
|
|
timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp,
|
|
response.status_code AS status_code,
|
|
COUNT(*) AS count
|
|
FROM
|
|
function_edge_logs
|
|
CROSS JOIN UNNEST(metadata) AS m
|
|
CROSS JOIN UNNEST(m.response) AS response
|
|
CROSS JOIN UNNEST(m.request) AS request
|
|
${
|
|
functionIds && functionIds.length > 0
|
|
? `WHERE function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})`
|
|
: ''
|
|
}
|
|
group by
|
|
timestamp,
|
|
status_code
|
|
order by
|
|
timestamp desc;
|
|
`
|
|
},
|
|
InvocationsByRegion: (interval, functionIds) => {
|
|
const granularity = analyticsIntervalToGranularity(interval)
|
|
return `
|
|
--edgefn-report-invocations-by-region
|
|
select
|
|
timestamp_trunc(timestamp, ${granularity}) as timestamp,
|
|
h.x_sb_edge_region as region,
|
|
count(*) as count
|
|
from
|
|
function_edge_logs
|
|
cross join unnest(metadata) as m
|
|
cross join unnest(m.response) as r
|
|
cross join unnest(r.headers) as h
|
|
where h.x_sb_edge_region is not null
|
|
${
|
|
functionIds && functionIds.length > 0
|
|
? `and function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})`
|
|
: ''
|
|
}
|
|
group by
|
|
timestamp,
|
|
region
|
|
order by
|
|
timestamp desc
|
|
`
|
|
},
|
|
ExecutionTime: (interval, functionIds) => {
|
|
const granularity = analyticsIntervalToGranularity(interval)
|
|
const hasFunctions = functionIds && functionIds.length > 0
|
|
return `
|
|
--edgefn-report-execution-time
|
|
select
|
|
timestamp_trunc(timestamp, ${granularity}) as timestamp,
|
|
${hasFunctions ? 'function_id,' : ''}
|
|
avg(m.execution_time_ms) as avg_execution_time
|
|
from
|
|
function_edge_logs
|
|
cross join unnest(metadata) as m
|
|
cross join unnest(m.request) as request
|
|
${hasFunctions ? `where function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` : ''}
|
|
group by
|
|
timestamp
|
|
${hasFunctions ? ', function_id' : ''}
|
|
order by
|
|
timestamp desc
|
|
`
|
|
},
|
|
}
|
|
|
|
/**
|
|
* FORMATTERS.
|
|
* Metrics need to be formatted before being passed on to the UI charts.
|
|
*/
|
|
|
|
function defaultFormatter(rawData: any, attributes: MultiAttribute[]) {
|
|
const chartAttributes = attributes
|
|
if (!rawData) return { data: undefined, chartAttributes }
|
|
const result = rawData.result || []
|
|
const timestamps = new Set<string>(result.map((p: any) => p.timestamp))
|
|
const data = Array.from(timestamps)
|
|
.sort()
|
|
.map((timestamp) => {
|
|
const point: any = { period_start: timestamp }
|
|
chartAttributes.forEach((attr) => {
|
|
point[attr.attribute] = 0
|
|
})
|
|
const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
|
|
matchingPoints.forEach((p: any) => {
|
|
point[attributes[0].attribute] = p.count
|
|
})
|
|
return point
|
|
})
|
|
return { data, chartAttributes }
|
|
}
|
|
|
|
const METRIC_FORMATTER: Record<
|
|
MetricKey,
|
|
(
|
|
rawData: any,
|
|
attributes: MultiAttribute[],
|
|
logsMetric: string,
|
|
functionIds?: string[],
|
|
edgeFnIdToName?: (id: string) => string | undefined
|
|
) => { data: any; chartAttributes: any }
|
|
> = {
|
|
TotalInvocations: (rawData, attributes, logsMetric, functionIds, edgeFnIdToName) => {
|
|
// Always use dynamic attributes, so the chart can show per-function stats.
|
|
if (!rawData) return { data: undefined, chartAttributes: attributes }
|
|
const result = rawData.result || []
|
|
|
|
const functionIdsInData = Array.from(new Set(result.map((p: any) => p.function_id))) as string[]
|
|
|
|
const chartFunctionIds = functionIds && functionIds.length > 0 ? functionIds : functionIdsInData
|
|
|
|
if (chartFunctionIds.length === 0) {
|
|
return { data: [], chartAttributes: [] } // No data, empty chart
|
|
}
|
|
|
|
const chartAttributes = chartFunctionIds.map((id: string) => ({
|
|
attribute: id,
|
|
label: edgeFnIdToName?.(id) ?? id,
|
|
provider: 'logs',
|
|
enabled: true,
|
|
}))
|
|
|
|
const timestamps = new Set<string>(result.map((p: any) => p.timestamp))
|
|
const data = Array.from(timestamps)
|
|
.sort()
|
|
.map((timestamp) => {
|
|
const point: any = { period_start: timestamp }
|
|
chartAttributes.forEach((attr) => {
|
|
point[attr.attribute] = 0
|
|
})
|
|
const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
|
|
matchingPoints.forEach((p: any) => {
|
|
point[p.function_id as string] = p.count
|
|
})
|
|
return point
|
|
})
|
|
|
|
return { data, chartAttributes }
|
|
},
|
|
ExecutionStatusCodes: (rawData, attributes) => {
|
|
if (!rawData) return { data: undefined, chartAttributes: attributes }
|
|
const result = rawData.result || []
|
|
|
|
const statusCodes = Array.from(new Set(result.map((p: any) => p.status_code)))
|
|
|
|
const chartAttributes = statusCodes.map((statusCode) => {
|
|
const statusCodeInfo = getHttpStatusCodeInfo(Number(statusCode))
|
|
const color =
|
|
REPORT_STATUS_CODE_COLORS[String(statusCode)] || REPORT_STATUS_CODE_COLORS.default
|
|
|
|
return {
|
|
attribute: `status_${statusCode}`,
|
|
label: `${statusCode} ${statusCodeInfo.label}`,
|
|
provider: 'logs',
|
|
enabled: true,
|
|
color: color,
|
|
statusCode: String(statusCode),
|
|
}
|
|
})
|
|
|
|
const timestamps = new Set<string>(result.map((p: any) => p.timestamp))
|
|
const data = Array.from(timestamps)
|
|
.sort()
|
|
.map((timestamp) => {
|
|
const point: any = { period_start: timestamp }
|
|
chartAttributes.forEach((attr) => {
|
|
point[attr.attribute] = 0
|
|
})
|
|
const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
|
|
matchingPoints.forEach((p: any) => {
|
|
point[`status_${p.status_code}`] = p.count
|
|
})
|
|
return point
|
|
})
|
|
|
|
return { data, chartAttributes }
|
|
},
|
|
InvocationsByRegion: (rawData, attributes) => {
|
|
if (!rawData) return { data: undefined, chartAttributes: attributes }
|
|
const result = rawData.result || []
|
|
|
|
const regions = Array.from(new Set(result.map((p: any) => p.region))).filter(Boolean)
|
|
|
|
if (regions.length === 0) {
|
|
return { data: [], chartAttributes: [] } // No data, empty chart
|
|
}
|
|
|
|
const chartAttributes = regions.map((region) => {
|
|
return {
|
|
attribute: region,
|
|
label: region,
|
|
provider: 'logs',
|
|
enabled: true,
|
|
}
|
|
})
|
|
|
|
const timestamps = new Set<string>(result.map((p: any) => p.timestamp))
|
|
const data = Array.from(timestamps)
|
|
.sort()
|
|
.map((timestamp) => {
|
|
const point: any = { period_start: timestamp }
|
|
chartAttributes.forEach((attr) => {
|
|
point[attr.attribute as string] = 0
|
|
})
|
|
const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
|
|
matchingPoints.forEach((p: any) => {
|
|
point[p.region] = p.count
|
|
})
|
|
return point
|
|
})
|
|
|
|
return { data, chartAttributes }
|
|
},
|
|
ExecutionTime: (rawData, attributes, logsMetric, functionIds, edgeFnIdToName) => {
|
|
if (!rawData) return { data: undefined, chartAttributes: attributes }
|
|
const result = rawData.result || []
|
|
const hasFunctions = functionIds && functionIds.length > 0
|
|
|
|
if (hasFunctions) {
|
|
const chartAttributes = functionIds.map((id: string) => ({
|
|
attribute: id,
|
|
label: edgeFnIdToName?.(id) ?? id,
|
|
provider: 'logs',
|
|
enabled: true,
|
|
}))
|
|
|
|
if (result.length === 0) {
|
|
return { data: [], chartAttributes }
|
|
}
|
|
|
|
const timestamps = new Set<string>(result.map((p: any) => p.timestamp))
|
|
const data = Array.from(timestamps)
|
|
.sort()
|
|
.map((timestamp) => {
|
|
const point: any = { period_start: timestamp }
|
|
chartAttributes.forEach((attr) => {
|
|
point[attr.attribute] = 0
|
|
})
|
|
const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
|
|
matchingPoints.forEach((p: any) => {
|
|
point[p.function_id as string] = p.avg_execution_time
|
|
})
|
|
return point
|
|
})
|
|
|
|
return { data, chartAttributes }
|
|
} else {
|
|
const chartAttributes = [
|
|
{
|
|
attribute: 'avg_execution_time',
|
|
label: 'Avg. execution time (ms)',
|
|
provider: 'logs',
|
|
enabled: true,
|
|
},
|
|
]
|
|
|
|
const data = result
|
|
.map((p: any) => ({
|
|
period_start: p.timestamp,
|
|
avg_execution_time: p.avg_execution_time,
|
|
}))
|
|
.sort(
|
|
(a: { period_start: string }, b: { period_start: string }) =>
|
|
new Date(a.period_start).getTime() - new Date(b.period_start).getTime()
|
|
)
|
|
|
|
return { data, chartAttributes }
|
|
}
|
|
},
|
|
}
|
|
|
|
/**
|
|
* REPORT QUERY.
|
|
* Fetching and state management for the report.
|
|
*/
|
|
|
|
export function useEdgeFunctionReport({
|
|
projectRef,
|
|
attributes,
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
enabled = true,
|
|
functionIds,
|
|
}: {
|
|
projectRef: string
|
|
attributes: MultiAttribute[]
|
|
startDate: string
|
|
endDate: string
|
|
interval: AnalyticsInterval
|
|
enabled?: boolean
|
|
functionIds?: string[]
|
|
}) {
|
|
const logsMetric = attributes.length > 0 ? attributes[0].attribute : ''
|
|
const { edgeFnIdToName } = useEdgeFnIdToName({ projectRef })
|
|
const isEdgeFnMetric = METRIC_KEYS.includes(logsMetric)
|
|
|
|
const sql = isEdgeFnMetric ? METRIC_SQL[logsMetric as MetricKey](interval, functionIds) : ''
|
|
|
|
const {
|
|
data: rawData,
|
|
error,
|
|
isLoading,
|
|
} = useQuery(
|
|
[
|
|
'edge-function-report',
|
|
projectRef,
|
|
logsMetric,
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
sql,
|
|
functionIds,
|
|
],
|
|
async () => {
|
|
const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, {
|
|
params: {
|
|
path: { ref: projectRef },
|
|
query: {
|
|
sql,
|
|
iso_timestamp_start: startDate,
|
|
iso_timestamp_end: endDate,
|
|
},
|
|
},
|
|
})
|
|
if (error) throw error
|
|
return data
|
|
},
|
|
{
|
|
enabled: Boolean(projectRef && sql && enabled && isEdgeFnMetric),
|
|
refetchOnWindowFocus: false,
|
|
}
|
|
)
|
|
|
|
// Use formatter if available
|
|
const formatter =
|
|
(isEdgeFnMetric ? METRIC_FORMATTER[logsMetric as MetricKey] : undefined) || defaultFormatter
|
|
const { data, chartAttributes } = formatter(
|
|
rawData,
|
|
attributes,
|
|
logsMetric,
|
|
functionIds,
|
|
edgeFnIdToName
|
|
)
|
|
|
|
return {
|
|
data,
|
|
attributes: chartAttributes,
|
|
isLoading,
|
|
error,
|
|
}
|
|
}
|