Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions apps/docs/content/guides/telemetry/reports.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,18 @@ The Storage report provides visibility into how your Supabase Storage is being u

The Realtime report tracks WebSocket connections, channel activity, and real-time event patterns in your Supabase project.

| Chart | Description | Key Insights |
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------- |
| Realtime Connections | Active WebSocket connections over time | Concurrent user activity and connection stability |
| Channel Events | Breakdown of broadcast, Postgres changes, and presence events | Real-time feature usage patterns |
| Rate of Channel Joins | Frequency of new channel subscriptions | User engagement with real-time features |
| Total Requests | HTTP requests to Realtime endpoints | API usage alongside WebSocket activity |
| Response Speed | Performance of Realtime API endpoints | Infrastructure optimization opportunities |
| Chart | Description | Key Insights |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------- |
| Realtime Connections | Active WebSocket connections over time | Concurrent user activity and connection stability |
| Broadcast Events | Broadcast events over time | Real-time feature usage patterns |
| Presence Events | Presence events over time | Real-time feature usage patterns |
| Postgres Changes Events | Postgres Changes events over time | Real-time feature usage patterns |
| Rate of Channel Joins | Frequency of new channel subscriptions | User engagement with real-time features |
| Message Payload Size | Median size of message payloads sent | Payload size that is being transmitted |
| Broadcast From Database Replication Lag | Median latency between database commit and broadcast when using broadcast from database | Latency to Broadcast from the database |
| Read/Write Private Channel Subscription RLS Execution Time | Median time to authorize private channels | `realtime.messages` RLS policies performance |
| Total Requests | HTTP requests to Realtime endpoints | API usage alongside WebSocket activity |
| Response Speed | Performance of Realtime API endpoints | Infrastructure optimization opportunities |

## Edge Functions

Expand Down
6 changes: 4 additions & 2 deletions apps/studio/components/interfaces/Reports/ReportWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface ReportWidgetProps<T = any> {
error?: string | Object | null
tooltip?: string | ReactNode
className?: string
contentClassName?: string
headerClassName?: string
renderer: (props: ReportWidgetRendererProps) => ReactNode
append?: (props: ReportWidgetRendererProps) => ReactNode
// for overriding props, such as data
Expand All @@ -38,8 +40,8 @@ const ReportWidget = (props: ReportWidgetProps) => {

return (
<Panel noMargin noHideOverflow className={cn('pb-0', props.className)} wrapWithLoading={false}>
<Panel.Content className="space-y-4">
<div className="flex flex-row items-start justify-between">
<Panel.Content className={cn('space-y-4', props.contentClassName)}>
<div className={cn('flex flex-row items-start justify-between', props.headerClassName)}>
<div className="gap-2">
<div className="flex flex-row gap-2">
<h3 className="w-full h-6">{props.title}</h3>{' '}
Expand Down
20 changes: 20 additions & 0 deletions apps/studio/components/interfaces/Reports/Reports.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,26 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
timestamp ASC
`,
},
requestsByCountry: {
queryType: 'logs',
sql: (filters) => `
-- reports-api-requests-by-country
select
cf.country as country,
count(t.id) as count
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(request.cf) as cf
where
cf.country is not null
${generateRegexpWhere(filters, false)}
group by
cf.country
`,
},
},
},
[Presets.AUTH]: {
Expand Down
257 changes: 256 additions & 1 deletion apps/studio/components/interfaces/Reports/renderers/ApiRenderers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sumBy from 'lodash/sumBy'
import { ChevronRight } from 'lucide-react'
import { Fragment, useState } from 'react'
import { Fragment, useRef, useState } from 'react'
import * as z from 'zod'

import { useParams } from 'common'
import {
Expand All @@ -25,6 +26,22 @@ import {
} from 'ui'
import { queryParamsToObject } from '../Reports.utils'
import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget'
import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps'
import { COUNTRY_LAT_LON } from 'components/interfaces/ProjectCreation/ProjectCreation.constants'
import { BASE_PATH } from 'lib/constants'
import { geoCentroid } from 'd3-geo'
import {
buildCountsByIso2,
getFillColor,
getFillOpacity,
isMicroCountry,
isKnownCountryCode,
computeMarkerRadius,
MAP_CHART_THEME,
extractIso2FromFeatureProps,
iso2ToCountryName,
} from 'components/interfaces/Reports/utils/geo'
import { useTheme } from 'next-themes'

export const NetworkTrafficRenderer = (
props: ReportWidgetProps<{
Expand Down Expand Up @@ -385,3 +402,241 @@ const RouteTdContent = (datum: RouteTdContentProps) => (
</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
)
export const RequestsByCountryMapRenderer = (
props: ReportWidgetProps<{
country: string | null
count: number
}>
) => {
const WORLD_TOPO_URL = `${BASE_PATH}/json/worldmap.json`
const containerRef = useRef<HTMLDivElement | null>(null)
const [hoverInfo, setHoverInfo] = useState<{
x: number
y: number
title: string
subtitle: string
visible: boolean
}>({ x: 0, y: 0, title: '', subtitle: '', visible: false })

const countsByIso2 = buildCountsByIso2(props.data)
const max = Object.values(countsByIso2).reduce((m, v) => (v > m ? v : m), 0)
const { resolvedTheme } = useTheme()
const theme = resolvedTheme === 'dark' ? MAP_CHART_THEME.dark : MAP_CHART_THEME.light

if (!!props.error) {
const AlertErrorSchema = z.object({ message: z.string() })
const parsed =
typeof props.error === 'string'
? { success: true, data: { message: props.error } }
: AlertErrorSchema.safeParse(props.error)
const alertError = parsed.success ? parsed.data : null
return <AlertError subject="Failed to retrieve requests by geography" error={alertError} />
}

return (
<div ref={containerRef} className="w-full h-[560px] relative border-t">
<ComposableMap
projection="geoMercator"
projectionConfig={{ scale: 155 }}
className="w-full h-full"
style={{ backgroundColor: theme.oceanFill }}
>
<ZoomableGroup minZoom={1} maxZoom={5} zoom={1.3}>
<Geographies geography={WORLD_TOPO_URL}>
{({ geographies }) => (
<>
{geographies.map((geo) => {
const title =
(geo.properties?.name as string) ||
(geo.properties?.NAME as string) ||
'Unknown'
const iso2 = extractIso2FromFeatureProps(
(geo.properties || undefined) as Record<string, unknown> | undefined
)
const value = iso2 ? countsByIso2[iso2] || 0 : 0
const baseOpacity = getFillOpacity(value, max, theme)
const tooltipTitle = title
const tooltipSubtitle = `${value.toLocaleString()} requests`
return (
<Geography
key={geo.rsmKey}
geography={geo}
onMouseMove={(e) => {
const rect = containerRef.current?.getBoundingClientRect()
const x = (rect ? e.clientX - rect.left : e.clientX) + 12
const y = (rect ? e.clientY - rect.top : e.clientY) + 12
setHoverInfo({
x,
y,
title: tooltipTitle,
subtitle: tooltipSubtitle,
visible: true,
})
}}
onMouseEnter={(e) => {
const rect = containerRef.current?.getBoundingClientRect()
const x = (rect ? e.clientX - rect.left : e.clientX) + 12
const y = (rect ? e.clientY - rect.top : e.clientY) + 12
setHoverInfo({
x,
y,
title: tooltipTitle,
subtitle: tooltipSubtitle,
visible: true,
})
}}
onMouseLeave={() => setHoverInfo((prev) => ({ ...prev, visible: false }))}
style={{
default: {
fill: getFillColor(value, max, theme),
stroke: theme.boundaryStroke,
strokeWidth: 0.4,
opacity: baseOpacity,
outline: 'none',
cursor: 'default',
},
hover: {
fill: getFillColor(value, max, theme),
stroke: 'transparent',
strokeWidth: 0,
opacity: Math.max(0, baseOpacity * 0.8),
outline: 'none',
cursor: 'default',
},
pressed: {
fill: getFillColor(value, max, theme),
stroke: 'transparent',
strokeWidth: 0,
opacity: Math.max(0, baseOpacity * 0.8),
outline: 'none',
cursor: 'default',
},
}}
aria-label={`${tooltipTitle} — ${tooltipSubtitle}`}
/>
)
})}

{geographies.map((geo) => {
const title =
(geo.properties?.name as string) ||
(geo.properties?.NAME as string) ||
'Unknown'
if (!isMicroCountry(title)) return null
const iso2 = extractIso2FromFeatureProps(
(geo.properties || undefined) as Record<string, unknown> | undefined
)
const value = iso2 ? countsByIso2[iso2] || 0 : 0
if (value <= 0) return null
const [lon, lat] = geoCentroid(geo)
const r = computeMarkerRadius(value, max)
const tooltipTitle = title
const tooltipSubtitle = `${value.toLocaleString()} requests`
return (
<Marker
key={`marker-${geo.rsmKey}`}
coordinates={[lon, lat]}
onMouseMove={(e) => {
const rect = containerRef.current?.getBoundingClientRect()
const x = (rect ? e.clientX - rect.left : e.clientX) + 12
const y = (rect ? e.clientY - rect.top : e.clientY) + 12
setHoverInfo({
x,
y,
title: tooltipTitle,
subtitle: tooltipSubtitle,
visible: true,
})
}}
onMouseEnter={(e) => {
const rect = containerRef.current?.getBoundingClientRect()
const x = (rect ? e.clientX - rect.left : e.clientX) + 12
const y = (rect ? e.clientY - rect.top : e.clientY) + 12
setHoverInfo({
x,
y,
title: tooltipTitle,
subtitle: tooltipSubtitle,
visible: true,
})
}}
onMouseLeave={() => setHoverInfo((prev) => ({ ...prev, visible: false }))}
>
<circle r={r} fill={theme.markerFill} />
</Marker>
)
})}

{(() => {
const present = new Set<string>()
for (const g of geographies) {
const code = extractIso2FromFeatureProps(
(g.properties || undefined) as Record<string, unknown> | undefined
)
if (code) present.add(code)
}

const markers: JSX.Element[] = []
for (const iso2 in countsByIso2) {
const count = countsByIso2[iso2]
if (count <= 0) continue
if (present.has(iso2)) continue
if (!isKnownCountryCode(iso2)) continue
const ll = COUNTRY_LAT_LON[iso2]
const r = computeMarkerRadius(count, max)
const tooltipTitle = iso2ToCountryName(iso2)
const tooltipSubtitle = `${count.toLocaleString()} requests`
markers.push(
<Marker
key={`fallback-${iso2}`}
coordinates={[ll.lon, ll.lat]}
onMouseMove={(e) => {
const rect = containerRef.current?.getBoundingClientRect()
const x = (rect ? e.clientX - rect.left : e.clientX) + 12
const y = (rect ? e.clientY - rect.top : e.clientY) + 12
setHoverInfo({
x,
y,
title: tooltipTitle,
subtitle: tooltipSubtitle,
visible: true,
})
}}
onMouseEnter={(e) => {
const rect = containerRef.current?.getBoundingClientRect()
const x = (rect ? e.clientX - rect.left : e.clientX) + 12
const y = (rect ? e.clientY - rect.top : e.clientY) + 12
setHoverInfo({
x,
y,
title: tooltipTitle,
subtitle: tooltipSubtitle,
visible: true,
})
}}
onMouseLeave={() => setHoverInfo((prev) => ({ ...prev, visible: false }))}
>
<circle r={r} fill={theme.markerFill} />
</Marker>
)
}

return markers
})()}
</>
)}
</Geographies>
</ZoomableGroup>
</ComposableMap>
{hoverInfo.visible && (
<div
className="pointer-events-none absolute z-10 rounded bg-surface-100 p-1.5 border border-surface-200 text-sm"
style={{ left: hoverInfo.x, top: hoverInfo.y }}
>
<h3 className="text-foreground-lighter text-sm">{hoverInfo.title}</h3>
<p className="text-foreground text-sm">{hoverInfo.subtitle}</p>
</div>
)}
</div>
)
}
Loading
Loading