mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 04:45:08 +03:00
## Changes - Replaces plan ID-based chart gating with entitlement-based checks across all observability pages (database, auth, realtime, edge-functions) - Adds requiredPlan display field so the upsell shows the correct target plan (e.g. "Upgrade to Team" for disk throughput, not just "Upgrade") - Decouples database-charts.ts from Organization Plan, now accepts pre-computed entitlement flags instead ### Note on `requiredPlan` The entitlement system currently tells us whether a user has access, but not what plan they need to get access. `requiredPlan` is a frontend-only display hint. Ideally the backend entitlement response would include a minimumPlan field so we can remove this hardcoded mapping. For now, this is a pragmatic workaround to avoid misleading upsell copy (e.g. a Free user seeing "Upgrade" for a Team-only feature and landing on Pro). ## Testing ### Database - Head to `/project/_/observability/database` with an Org on the Free Plan and assert that the Disk throughput is gated. <img width="1402" height="682" alt="image" src="https://github.com/user-attachments/assets/b783d866-774d-44dc-88d1-797d26502f02" /> ### Auth - Head to `/project/_/observability/auth` with an Org on the Free Plan and assert that the following metrics are gated: <img width="1402" height="682" alt="image" src="https://github.com/user-attachments/assets/cb062dd7-5864-4e40-a11f-ad6be1f7fd41" /> ### Edge Functions - Head to `/project/_/observability/edge-functions` with an Org on the Free Plan and assert that the following metrics are gated: <img width="1402" height="320" alt="image" src="https://github.com/user-attachments/assets/517b5483-4575-493c-9401-2c67ac9e684c" /> ### Realtime - Head to `/project/_/observability/realtime` with an Org on the Free Plan and assert that the following metrics are gated: <img width="1402" height="722" alt="image" src="https://github.com/user-attachments/assets/0c2757a2-8482-43f4-a65c-c0dde3d878c7" />
392 lines
9.8 KiB
TypeScript
392 lines
9.8 KiB
TypeScript
import type { AnalyticsData, AnalyticsInterval } from 'data/analytics/constants'
|
|
import { mapResponseToAnalyticsData } from 'data/analytics/infra-monitoring-queries'
|
|
import {
|
|
getInfraMonitoringAttributes,
|
|
InfraMonitoringAttribute,
|
|
} from 'data/analytics/infra-monitoring-query'
|
|
import { ReportConfig } from './reports.types'
|
|
|
|
async function runInfraMonitoringQuery(
|
|
projectRef: string,
|
|
attribute: InfraMonitoringAttribute,
|
|
startDate: string,
|
|
endDate: string,
|
|
interval: AnalyticsInterval,
|
|
databaseIdentifier?: string
|
|
): Promise<AnalyticsData> {
|
|
const response = await getInfraMonitoringAttributes({
|
|
projectRef,
|
|
attributes: [attribute],
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier,
|
|
})
|
|
|
|
// Use shared mapping function that handles both single and multi-attribute response formats
|
|
const mapped = mapResponseToAnalyticsData(response, [attribute])
|
|
const analyticsData = mapped[attribute]
|
|
|
|
return {
|
|
data: analyticsData?.data ?? [],
|
|
format: analyticsData?.format,
|
|
yAxisLimit: analyticsData?.yAxisLimit,
|
|
total: analyticsData?.total,
|
|
totalGrouped: { [attribute]: analyticsData?.total },
|
|
} as AnalyticsData
|
|
}
|
|
|
|
export const realtimeReports = ({
|
|
projectRef,
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier,
|
|
}: {
|
|
projectRef: string
|
|
startDate: string
|
|
endDate: string
|
|
interval: AnalyticsInterval
|
|
databaseIdentifier?: string
|
|
}): ReportConfig[] => [
|
|
{
|
|
id: 'realtime_sum_connections_connected',
|
|
label: 'Connected Clients',
|
|
valuePrecision: 0,
|
|
hide: false,
|
|
showSumAsDefaultHighlight: false,
|
|
showNewBadge: true,
|
|
showTooltip: true,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'line',
|
|
titleTooltip: 'Total number of connected realtime clients.',
|
|
dataProvider: async () => {
|
|
const data = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_sum_connections_connected',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_sum_connections_connected',
|
|
label: 'Connected Clients',
|
|
},
|
|
]
|
|
|
|
return { data: data?.data || [], attributes }
|
|
},
|
|
},
|
|
{
|
|
id: 'broadcast-events',
|
|
label: 'Broadcast Events',
|
|
valuePrecision: 0,
|
|
hide: false,
|
|
showNewBadge: true,
|
|
showTooltip: true,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'bar',
|
|
titleTooltip: '',
|
|
dataProvider: async () => {
|
|
const { data } = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_channel_events',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const transformedData = data?.map((p) => ({
|
|
...p,
|
|
realtime_channel_events: Number(p.realtime_channel_events) || 0,
|
|
}))
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_channel_events',
|
|
label: 'Events',
|
|
},
|
|
]
|
|
|
|
return { data: transformedData || [], attributes }
|
|
},
|
|
},
|
|
|
|
{
|
|
id: 'presence-events',
|
|
label: 'Presence Events',
|
|
valuePrecision: 0,
|
|
hide: false,
|
|
showNewBadge: true,
|
|
showTooltip: true,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'bar',
|
|
titleTooltip: '',
|
|
dataProvider: async () => {
|
|
const { data } = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_channel_presence_events',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const transformedData = data?.map((p) => ({
|
|
...p,
|
|
realtime_channel_presence_events: Number(p.realtime_channel_presence_events) || 0,
|
|
}))
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_channel_presence_events',
|
|
label: 'Events',
|
|
},
|
|
]
|
|
|
|
return { data: transformedData || [], attributes }
|
|
},
|
|
},
|
|
|
|
{
|
|
id: 'db-events',
|
|
label: 'Postgres Changes Events',
|
|
valuePrecision: 0,
|
|
hide: false,
|
|
showNewBadge: true,
|
|
showTooltip: true,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'bar',
|
|
titleTooltip: '',
|
|
dataProvider: async () => {
|
|
const { data } = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_channel_db_events',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const transformedData = data?.map((p) => ({
|
|
...p,
|
|
realtime_channel_db_events: Number(p.realtime_channel_db_events) || 0,
|
|
}))
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_channel_db_events',
|
|
label: 'Events',
|
|
},
|
|
]
|
|
|
|
return { data: transformedData || [], attributes }
|
|
},
|
|
},
|
|
{
|
|
id: 'realtime_rate_of_channel_joins',
|
|
label: 'Rate of Channel Joins',
|
|
valuePrecision: 2,
|
|
hide: false,
|
|
showSumAsDefaultHighlight: false,
|
|
showTooltip: false,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'line',
|
|
titleTooltip: '',
|
|
dataProvider: async () => {
|
|
const data = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_channel_joins',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_channel_joins',
|
|
label: 'Channel Joins',
|
|
},
|
|
]
|
|
|
|
return { data: data?.data || [], attributes }
|
|
},
|
|
},
|
|
{
|
|
id: 'realtime_payload_size',
|
|
label: 'Message Payload Size',
|
|
valuePrecision: 2,
|
|
showNewBadge: true,
|
|
hide: false,
|
|
showSumAsDefaultHighlight: false,
|
|
showTooltip: true,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'line',
|
|
titleTooltip: 'Median size of message payloads sent',
|
|
YAxisProps: {
|
|
width: 50,
|
|
tickFormatter: (value: number) => `${value}B`,
|
|
},
|
|
format: (value: unknown) => `${Number(value).toFixed(2)}B`,
|
|
dataProvider: async () => {
|
|
const data = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_payload_size',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_payload_size',
|
|
label: 'Median Payload Size (bytes)',
|
|
},
|
|
]
|
|
|
|
return { data: data?.data || [], attributes }
|
|
},
|
|
},
|
|
{
|
|
id: 'realtime_replication_connection_lag',
|
|
label: 'Broadcast From Database Replication Lag',
|
|
valuePrecision: 2,
|
|
showNewBadge: true,
|
|
hide: false,
|
|
showSumAsDefaultHighlight: false,
|
|
showTooltip: true,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'line',
|
|
titleTooltip:
|
|
'Median time between database commit and broadcast when using broadcast from database.',
|
|
entitlement: 'realtime',
|
|
requiredPlan: 'Pro',
|
|
YAxisProps: {
|
|
width: 50,
|
|
tickFormatter: (value: number) => `${value}ms`,
|
|
},
|
|
format: (value: unknown) => `${Number(value).toFixed(2)}ms`,
|
|
dataProvider: async () => {
|
|
const data = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_replication_connection_lag',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_replication_connection_lag',
|
|
label: 'Median Replication Lag (ms)',
|
|
},
|
|
]
|
|
|
|
return { data: data?.data || [], attributes }
|
|
},
|
|
},
|
|
{
|
|
id: 'realtime_read_authorization_rls_execution_time',
|
|
label: '(Read) Private Channel Subscription RLS Execution Time',
|
|
valuePrecision: 2,
|
|
showNewBadge: true,
|
|
hide: false,
|
|
showSumAsDefaultHighlight: false,
|
|
showTooltip: true,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'line',
|
|
titleTooltip:
|
|
'Execution median time of RLS (Row Level Security) to subscribe to a private channel',
|
|
entitlement: 'realtime',
|
|
requiredPlan: 'Pro',
|
|
YAxisProps: {
|
|
width: 50,
|
|
tickFormatter: (value: number) => `${value}ms`,
|
|
},
|
|
format: (value: unknown) => `${Number(value).toFixed(2)}ms`,
|
|
dataProvider: async () => {
|
|
const data = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_read_authorization_rls_execution_time',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_read_authorization_rls_execution_time',
|
|
label: 'RLS Execution Time (ms)',
|
|
},
|
|
]
|
|
|
|
return { data: data?.data || [], attributes }
|
|
},
|
|
},
|
|
{
|
|
id: 'realtime_write_authorization_rls_execution_time',
|
|
label: '(Write) Private Channel Subscription RLS Execution Time',
|
|
valuePrecision: 2,
|
|
showNewBadge: true,
|
|
hide: false,
|
|
showSumAsDefaultHighlight: false,
|
|
showTooltip: true,
|
|
showLegend: false,
|
|
showMaxValue: false,
|
|
hideChartType: false,
|
|
defaultChartStyle: 'line',
|
|
titleTooltip:
|
|
'Execution median time of RLS (Row Level Security) to publish to a private channel',
|
|
entitlement: 'realtime',
|
|
requiredPlan: 'Pro',
|
|
YAxisProps: {
|
|
width: 50,
|
|
tickFormatter: (value: number) => `${value}ms`,
|
|
},
|
|
format: (value: unknown) => `${Number(value).toFixed(2)}ms`,
|
|
dataProvider: async () => {
|
|
const data = await runInfraMonitoringQuery(
|
|
projectRef,
|
|
'realtime_write_authorization_rls_execution_time',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
databaseIdentifier
|
|
)
|
|
|
|
const attributes = [
|
|
{
|
|
attribute: 'realtime_write_authorization_rls_execution_time',
|
|
label: 'RLS Execution Time (ms)',
|
|
},
|
|
]
|
|
|
|
return { data: data?.data || [], attributes }
|
|
},
|
|
},
|
|
]
|