Files
supabase/apps/studio/data/reports/v2/realtime.config.ts
Ignacio Dobronich 60a2a5e092 chore: observability dashboard entitlements (#43502)
## 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"
/>
2026-03-11 16:57:53 -03:00

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 }
},
},
]