mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## Problem The org Usage page links added in #50570 point to `/docs/guides/platform/manage-your-usage/log-ingest` and `/log-query`. Neither page exists. The docs live at `logs-ingest` and `logs-query`, so every click from the Usage page lands on a 404. Within a day of #50570 shipping, these two paths were the top docs 404s by unique visitors. ## Solution - Point both Usage page links at the `logs-*` pages. - Add permanent redirects from the singular paths, because they're already being shared: search engines and AI assistants now send people to them. The existing generator in `apps/www/next.config.mjs` adds the `.md` variants. ## To test On the Vercel previews: - [x] Open an org's Usage page on the Studio preview and click the Log Ingestion docs link: expect the `logs-ingest` docs page, not a 404. Opens `supabase.com/docs/guides/platform/manage-your-usage/logs-ingest` in a new tab ("Manage Logs Ingest usage"). - [x] Click the Log Query docs link: expect the `logs-query` docs page. Opens `.../logs-query` in a new tab ("Manage Logs Query usage"). - [x] No docs link on the Usage page still points at the singular `log-ingest` or `log-query` paths. - [x] On the www preview, request `/docs/guides/platform/manage-your-usage/log-ingest` and `/log-query`: expect a 308 to the `logs-*` pages. Both return 308 to the matching `logs-*` path. The www preview doesn't serve `/docs`, so I confirmed both targets return 200 on production. - [x] `log-ingest.md` and `log-query.md` also return 308 to the matching `logs-*.md` paths. ## Linear - fixes GROWTH-1296 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Updated the Log Ingestion and Log Query documentation links to their current pages. * Added permanent redirects from the previous documentation paths to the corresponding pages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
469 lines
19 KiB
TypeScript
469 lines
19 KiB
TypeScript
import { ReactNode } from 'react'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
|
|
import { USAGE_APPROACHING_THRESHOLD } from '@/components/interfaces/Billing/Billing.constants'
|
|
import { EgressType, PricingMetric } from '@/data/analytics/org-daily-stats-query'
|
|
import type { OrgSubscription } from '@/data/subscriptions/types'
|
|
import type { OrgUsageResponse } from '@/data/usage/org-usage-query'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
|
|
export const COLOR_MAP = {
|
|
white: { bar: 'fill-foreground', marker: 'bg-foreground' },
|
|
green: { bar: 'fill-green-800', marker: 'bg-green-800' },
|
|
'dark-green': { bar: 'fill-green-1000', marker: 'bg-green-1000' },
|
|
blue: { bar: 'fill-blue-900', marker: 'bg-blue-900' },
|
|
yellow: { bar: 'fill-yellow-800', marker: 'bg-yellow-800' },
|
|
'dark-yellow': { bar: 'fill-yellow-1000', marker: 'bg-yellow-1000' },
|
|
orange: { bar: 'fill-orange-800', marker: 'bg-orange-800' },
|
|
'dark-orange': { bar: 'fill-orange-1000', marker: 'bg-orange-1100' },
|
|
teal: { bar: 'fill-teal-600', marker: 'bg-teal-700' },
|
|
red: { bar: 'fill-red-800', marker: 'bg-red-800' },
|
|
'dark-red': { bar: 'fill-red-1000', marker: 'bg-red-1000' },
|
|
purple: { bar: 'fill-purple-900', marker: 'bg-purple-900' },
|
|
}
|
|
|
|
export const USAGE_STATUS = {
|
|
NORMAL: 'NORMAL',
|
|
APPROACHING: 'APPROACHING',
|
|
EXCEEDED: 'EXCEEDED',
|
|
}
|
|
|
|
export type AttributeColor =
|
|
| 'white'
|
|
| 'blue'
|
|
| 'green'
|
|
| 'yellow'
|
|
| 'orange'
|
|
| 'purple'
|
|
| 'red'
|
|
| 'dark-red'
|
|
| 'dark-orange'
|
|
| 'dark-yellow'
|
|
| 'dark-green'
|
|
| 'teal'
|
|
|
|
export interface Attribute {
|
|
key: string
|
|
name?: string
|
|
color: AttributeColor
|
|
}
|
|
export interface CategoryAttribute {
|
|
anchor: string
|
|
key: string // Property from organization usage
|
|
attributes: Attribute[] // For querying against stats-daily / infra-monitoring
|
|
name: string
|
|
unit: 'bytes' | 'absolute' | 'percentage' | 'hours' | 'gigabytes'
|
|
links?: {
|
|
name: string
|
|
url: string
|
|
}[]
|
|
description: string
|
|
chartPrefix?: 'Max' | 'Average' | 'Cumulative'
|
|
chartSuffix?: string
|
|
chartDescription: string
|
|
additionalInfo?: (usage?: OrgUsageResponse) => ReactNode | null
|
|
}
|
|
|
|
export type CategoryMetaKey = 'egress' | 'sizeCount' | 'activity' | 'compute' | 'logs' | 'pipelines'
|
|
|
|
export interface CategoryMeta {
|
|
key: CategoryMetaKey
|
|
name: string
|
|
description: string
|
|
attributes: CategoryAttribute[]
|
|
}
|
|
|
|
export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[] = (
|
|
subscription
|
|
) => {
|
|
const egressAttributes: CategoryAttribute[] = [
|
|
{
|
|
anchor: 'egress',
|
|
key: PricingMetric.EGRESS,
|
|
attributes: [
|
|
{ key: EgressType.AUTH, name: 'Auth Egress', color: 'yellow' },
|
|
{ key: EgressType.REST, name: 'PostgREST Egress', color: 'green' },
|
|
{ key: EgressType.STORAGE, name: 'Storage Egress', color: 'blue' },
|
|
{ key: EgressType.REALTIME, name: 'Realtime Egress', color: 'orange' },
|
|
{ key: EgressType.FUNCTIONS, name: 'Functions Egress', color: 'purple' },
|
|
{ key: EgressType.SUPAVISOR, name: 'Shared Pooler Egress', color: 'red' },
|
|
{ key: EgressType.LOGDRAIN, name: 'Logdrain Egress', color: 'teal' },
|
|
],
|
|
name: 'Egress',
|
|
unit: 'bytes',
|
|
description:
|
|
'Contains any outgoing traffic including Database, Storage, Realtime, Auth, API, Edge Functions, Pooler and Log Drains.\nBilling is based on the total sum of uncached egress in GB throughout your billing period.\nEgress via cache hits is billed separately.',
|
|
chartDescription:
|
|
'The breakdown of different egress types is inclusive of cached egress, even though it is billed separately. The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Documentation',
|
|
url: `${DOCS_URL}/guides/platform/manage-your-usage/egress`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
anchor: 'cachedEgress',
|
|
key: PricingMetric.CACHED_EGRESS,
|
|
attributes: [{ key: PricingMetric.CACHED_EGRESS.toLowerCase(), color: 'white' }],
|
|
name: 'Cached Egress',
|
|
unit: 'bytes',
|
|
description:
|
|
'Contains any outgoing traffic that is served from a cache hit. Includes API, Storage and Edge Functions.\nBilling is based on the total sum of cached egress in GB throughout your billing period.',
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Documentation',
|
|
url: `${DOCS_URL}/guides/platform/manage-your-usage/egress`,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
|
|
const databaseAndStorageSizeAttributes: CategoryAttribute[] = []
|
|
if (subscription?.plan.id === 'free') {
|
|
databaseAndStorageSizeAttributes.push({
|
|
anchor: 'dbSize',
|
|
key: PricingMetric.DATABASE_SIZE,
|
|
attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }],
|
|
name: 'Database size',
|
|
chartPrefix: 'Average',
|
|
unit: 'bytes',
|
|
description:
|
|
'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres.',
|
|
links: [
|
|
{
|
|
name: 'Documentation',
|
|
url: `${DOCS_URL}/guides/platform/database-size`,
|
|
},
|
|
],
|
|
chartDescription: 'The data refreshes every hour.',
|
|
additionalInfo: (usage?: OrgUsageResponse) => {
|
|
const usageMeta = usage?.usages.find((x) => x.metric === PricingMetric.DATABASE_SIZE)
|
|
const usageRatio =
|
|
typeof usageMeta !== 'number'
|
|
? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0)
|
|
: 0
|
|
const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0
|
|
|
|
const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD
|
|
const isExceededLimit = hasLimit && usageRatio >= 1
|
|
const isCapped = usageMeta?.capped
|
|
|
|
const onFreePlan = subscription?.plan?.name === 'Free'
|
|
|
|
return (
|
|
<div>
|
|
{(isApproachingLimit || isExceededLimit) && isCapped && (
|
|
<Admonition
|
|
type={isExceededLimit ? 'danger' : 'warning'}
|
|
title={
|
|
isExceededLimit ? 'Exceeding database size limit' : 'Nearing database size limit'
|
|
}
|
|
>
|
|
<div className="flex w-full items-center flex-col justify-center space-y-2 md:flex-row md:justify-between">
|
|
<div>
|
|
When you reach your database size limit, your project can go into read-only
|
|
mode.{' '}
|
|
{onFreePlan
|
|
? 'Please upgrade your Plan.'
|
|
: "Disable your spend cap to scale seamlessly, and pay for over-usage beyond your Plan's quota."}
|
|
</div>
|
|
</div>
|
|
</Admonition>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
})
|
|
} else if (subscription?.plan.id !== 'platform') {
|
|
databaseAndStorageSizeAttributes.push({
|
|
anchor: 'diskSize',
|
|
key: 'diskSize',
|
|
attributes: [],
|
|
name: 'Disk size',
|
|
chartPrefix: 'Average',
|
|
unit: 'bytes',
|
|
description:
|
|
"Each Supabase project comes with a dedicated disk. Each project gets 8 GB of disk for free. Billing is based on the provisioned disk size. Disk automatically scales up when you get close to it's size.\nEach hour your project is using more than 8 GB of GP3 disk, it incurs the overages in GB-Hrs, i.e. a 16 GB disk incurs 8 GB-Hrs every hour. Extra disk size costs $0.125/GB/month ($0.000171/GB-Hr).",
|
|
links: [
|
|
{
|
|
name: 'Documentation',
|
|
url: `${DOCS_URL}/guides/platform/manage-your-usage/disk-size`,
|
|
},
|
|
{
|
|
name: 'Disk Management',
|
|
url: `${DOCS_URL}/guides/platform/database-size#disk-management`,
|
|
},
|
|
],
|
|
chartDescription: '',
|
|
})
|
|
} else if (subscription?.plan.id === 'platform') {
|
|
databaseAndStorageSizeAttributes.push({
|
|
anchor: 'databaseSize',
|
|
key: PricingMetric.DATABASE_SIZE,
|
|
attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }],
|
|
name: 'Database Size',
|
|
chartPrefix: 'Cumulative',
|
|
unit: 'bytes',
|
|
description:
|
|
'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres.\nBilling is prorated down to the hour and will be displayed GB-Hrs.',
|
|
chartDescription: 'The data refreshes every hour.',
|
|
})
|
|
}
|
|
|
|
databaseAndStorageSizeAttributes.push({
|
|
anchor: 'storageSize',
|
|
key: PricingMetric.STORAGE_SIZE,
|
|
attributes: [{ key: PricingMetric.STORAGE_SIZE.toLowerCase(), color: 'white' }],
|
|
name: 'Storage Size',
|
|
chartPrefix: 'Average',
|
|
unit: 'bytes',
|
|
description:
|
|
'Sum of all objects in your storage buckets.\nBilling is prorated down to the hour and will be displayed GB-Hrs.',
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Storage',
|
|
url: `${DOCS_URL}/guides/storage`,
|
|
},
|
|
],
|
|
})
|
|
|
|
const logStorageAttributes: CategoryAttribute[] =
|
|
subscription?.plan.id === 'platform'
|
|
? [
|
|
{
|
|
anchor: 'logStorage',
|
|
key: PricingMetric.LOG_STORAGE,
|
|
attributes: [{ key: PricingMetric.LOG_STORAGE.toLowerCase(), color: 'white' }],
|
|
name: 'Log Storage',
|
|
unit: 'absolute',
|
|
description:
|
|
'Total amount of logs stored on the platform. Log retention depends on your platform agreement.\nBilling is based on the total amount of logs stored and factors in the retention period.',
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [],
|
|
},
|
|
]
|
|
: []
|
|
|
|
return [
|
|
{
|
|
key: 'egress',
|
|
name: 'Egress',
|
|
description: 'Amount of data transmitted over all network connections',
|
|
attributes: egressAttributes,
|
|
},
|
|
{
|
|
key: 'sizeCount',
|
|
name: 'Database & Storage Size',
|
|
description: 'Amount of resources your project is consuming',
|
|
attributes: databaseAndStorageSizeAttributes,
|
|
},
|
|
{
|
|
key: 'activity',
|
|
name: 'Activity',
|
|
description: 'Usage statistics that reflect the activity of your project',
|
|
attributes: [
|
|
{
|
|
anchor: 'mau',
|
|
key: PricingMetric.MONTHLY_ACTIVE_USERS,
|
|
attributes: [{ key: PricingMetric.MONTHLY_ACTIVE_USERS.toLowerCase(), color: 'white' }],
|
|
name: 'Monthly Active Users',
|
|
chartPrefix: 'Cumulative',
|
|
chartSuffix: 'in billing period',
|
|
unit: 'absolute',
|
|
description:
|
|
'Users who log in or refresh their token count towards MAU.\nBilling is based on the sum of distinct users requesting your API throughout the billing period. Resets every billing cycle.',
|
|
chartDescription:
|
|
'The data is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.',
|
|
links: [
|
|
{
|
|
name: 'Auth',
|
|
url: `${DOCS_URL}/guides/auth`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
anchor: 'mauSso',
|
|
key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS,
|
|
attributes: [
|
|
{ key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS.toLowerCase(), color: 'white' },
|
|
],
|
|
name: 'Monthly Active SSO Users',
|
|
chartPrefix: 'Cumulative',
|
|
chartSuffix: 'in billing period',
|
|
unit: 'absolute',
|
|
description:
|
|
'SSO users who log in or refresh their token count towards SSO MAU.\nBilling is based on the sum of distinct Single Sign-On users requesting your API throughout the billing period. Resets every billing cycle.',
|
|
chartDescription:
|
|
'The data refreshes over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.',
|
|
links: [
|
|
{
|
|
name: 'SSO with SAML 2.0',
|
|
url: `${DOCS_URL}/guides/auth/sso/auth-sso-saml`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
anchor: 'storageImageTransformations',
|
|
key: PricingMetric.STORAGE_IMAGES_TRANSFORMED,
|
|
attributes: [
|
|
{ key: PricingMetric.STORAGE_IMAGES_TRANSFORMED.toLowerCase(), color: 'white' },
|
|
],
|
|
name: 'Storage Image Transformations',
|
|
chartPrefix: 'Cumulative',
|
|
chartSuffix: 'in billing period',
|
|
unit: 'absolute',
|
|
description:
|
|
'We count all images that were transformed in the billing period, ignoring any transformations.\nUsage example: You transform one image with four different size transformations and another image with just a single transformation. It counts as two, as only two images were transformed.\nBilling is based on the count of (origin) images that used transformations throughout the billing period. Resets every billing cycle.',
|
|
chartDescription:
|
|
'The data refreshes every 24 hours.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.',
|
|
links: [
|
|
{
|
|
name: 'Documentation',
|
|
url: `${DOCS_URL}/guides/storage/image-transformations`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
anchor: 'funcInvocations',
|
|
key: PricingMetric.FUNCTION_INVOCATIONS,
|
|
attributes: [{ key: PricingMetric.FUNCTION_INVOCATIONS.toLowerCase(), color: 'white' }],
|
|
name: 'Edge Function Invocations',
|
|
unit: 'absolute',
|
|
description:
|
|
'Every serverless function invocation independent of response status is counted.\nBilling is based on the sum of all invocations throughout your billing period.',
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Edge Functions',
|
|
url: `${DOCS_URL}/guides/functions`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
anchor: 'realtimeMessageCount',
|
|
key: PricingMetric.REALTIME_MESSAGE_COUNT,
|
|
attributes: [{ key: PricingMetric.REALTIME_MESSAGE_COUNT.toLowerCase(), color: 'white' }],
|
|
name: 'Realtime Messages',
|
|
unit: 'absolute',
|
|
description:
|
|
"Count of messages going through Realtime. Includes database changes, broadcast and presence. \nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.",
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Realtime Limits',
|
|
url: `${DOCS_URL}/guides/realtime/limits`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
anchor: 'realtimePeakConnections',
|
|
key: PricingMetric.REALTIME_PEAK_CONNECTIONS,
|
|
attributes: [
|
|
{ key: PricingMetric.REALTIME_PEAK_CONNECTIONS.toLowerCase(), color: 'white' },
|
|
],
|
|
name: 'Realtime Concurrent Peak Connections',
|
|
chartPrefix: 'Max',
|
|
unit: 'absolute',
|
|
description:
|
|
'Total number of successful connections. Connections attempts are not counted towards usage.\nBilling is based on the maximum amount of concurrent peak connections throughout your billing period.',
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Realtime Limits',
|
|
url: `${DOCS_URL}/guides/realtime/limits`,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
{
|
|
key: 'logs',
|
|
name: 'Logs',
|
|
description: 'Usage statistics related to your logs',
|
|
attributes: [
|
|
{
|
|
anchor: 'logIngestion',
|
|
key: PricingMetric.LOG_INGESTION,
|
|
attributes: [{ key: PricingMetric.LOG_INGESTION.toLowerCase(), color: 'white' }],
|
|
name: 'Log Ingestion',
|
|
unit: 'bytes',
|
|
description:
|
|
subscription?.plan.id === 'platform'
|
|
? "Total volume of log data that Supabase ingests across all your project's services (Postgres, API gateway, Auth, Storage, Realtime, Edge Functions, and others) during the billing cycle."
|
|
: "Total volume of log data that Supabase ingests across all your project's services (Postgres, API gateway, Auth, Storage, Realtime, Edge Functions, and others) during the billing cycle.\nBilling starts after the grace period ends at the start of 2027.",
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Log Ingestion',
|
|
url: `${DOCS_URL}/guides/platform/manage-your-usage/logs-ingest`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
anchor: 'logQuery',
|
|
key: PricingMetric.LOG_QUERYING,
|
|
attributes: [{ key: PricingMetric.LOG_QUERYING.toLowerCase(), color: 'white' }],
|
|
name: 'Log Query',
|
|
unit: 'bytes',
|
|
description:
|
|
subscription?.plan.id === 'platform'
|
|
? 'Total amount of logs queried across all projects.\nBilling is based on the total amount of logs queried in Gigabyte.'
|
|
: "Total amount of logs queried across all projects.\nLogs Query usage isn't billed directly. Instead, your organization gets a log query allowance that scales with how much log data you ingest. The allowance covers the volume of log data scanned when you read logs through the Studio UI, the Management API, the CLI, or any other interface.\nEnforcement starts after the grace period ends at the start of 2027.",
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Log Query',
|
|
url: `${DOCS_URL}/guides/platform/manage-your-usage/logs-query`,
|
|
},
|
|
],
|
|
},
|
|
...logStorageAttributes,
|
|
],
|
|
},
|
|
|
|
{
|
|
key: 'pipelines',
|
|
name: 'Pipelines',
|
|
description: 'Usage statistics related to your pipelines',
|
|
attributes: [
|
|
{
|
|
anchor: 'pipeline-initial-sync-data',
|
|
key: PricingMetric.ETL_COPY_BACKFILL_DATA,
|
|
attributes: [{ key: PricingMetric.ETL_COPY_BACKFILL_DATA.toLowerCase(), color: 'white' }],
|
|
name: 'Initial Sync Data Processed',
|
|
unit: 'bytes',
|
|
description:
|
|
'Postgres row data accepted by pipeline destinations during initial sync or resynchronization across all projects.\nBilling is based on the total amount of initial sync data processed in gigabytes.',
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Pipelines',
|
|
url: `${DOCS_URL}/guides/platform/manage-your-usage/pipelines`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
anchor: 'pipeline-ongoing-replication-data',
|
|
key: PricingMetric.ETL_REPLICATED_DATA,
|
|
attributes: [{ key: PricingMetric.ETL_REPLICATED_DATA.toLowerCase(), color: 'white' }],
|
|
name: 'Ongoing Replication Data Processed',
|
|
unit: 'bytes',
|
|
description:
|
|
'Postgres row data accepted by pipeline destinations during ongoing replication across all projects.\nBilling is based on the total amount of ongoing replication data processed in gigabytes.',
|
|
chartDescription: 'The data refreshes every hour.',
|
|
links: [
|
|
{
|
|
name: 'Pipelines',
|
|
url: `${DOCS_URL}/guides/platform/manage-your-usage/pipelines`,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}
|