mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat: log query/ingest insights (#50570)
We are doing a soft rollout for log pricing including log ingest and log querying. We currently only want to display usage/soft warnings, which is why the metrics are filtered out in some components.
This commit is contained in:
1 parent
ad5c4bb879
commit
a5ad2ce745
12 files changed
+331
-260
No files matched your search
+184
-177
@@ -1,4 +1,5 @@
|
||||
import { PricingMetric } from '@/data/analytics/org-daily-stats-query'
|
||||
import { OrgSubscription } from '@/data/subscriptions/types'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
export const USAGE_APPROACHING_THRESHOLD = 0.8
|
||||
@@ -17,181 +18,187 @@ export interface Metric {
|
||||
}
|
||||
}
|
||||
|
||||
export const BILLING_BREAKDOWN_METRICS: Metric[] = [
|
||||
{
|
||||
key: PricingMetric.DATABASE_SIZE,
|
||||
name: 'Database Size',
|
||||
units: 'bytes',
|
||||
anchor: 'dbSize',
|
||||
category: 'Database',
|
||||
unitName: 'GB',
|
||||
tip: 'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres. This measurement reflects your current database size at this moment. Free Plan projects are limited to 0.5 GB Database Size each and are monitored across the entire billing period.',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.EGRESS,
|
||||
name: 'Egress',
|
||||
units: 'bytes',
|
||||
anchor: 'egress',
|
||||
category: 'Database',
|
||||
unitName: 'GB',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.CACHED_EGRESS,
|
||||
name: 'Cached Egress',
|
||||
units: 'bytes',
|
||||
anchor: 'cachedEgress',
|
||||
category: 'Database',
|
||||
unitName: 'GB',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.MONTHLY_ACTIVE_USERS,
|
||||
name: 'Monthly Active Users',
|
||||
units: 'absolute',
|
||||
anchor: 'mau',
|
||||
category: 'Authentication',
|
||||
unitName: 'MAU',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS,
|
||||
name: 'Monthly Active SSO Users',
|
||||
units: 'absolute',
|
||||
anchor: 'mauSso',
|
||||
category: 'Authentication',
|
||||
unitName: 'MAU',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.MONTHLY_ACTIVE_THIRD_PARTY_USERS,
|
||||
name: 'Monthly Active Third-Party Users',
|
||||
units: 'absolute',
|
||||
category: 'Authentication',
|
||||
unitName: 'MAU',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.STORAGE_SIZE,
|
||||
name: 'Storage Size',
|
||||
units: 'bytes',
|
||||
anchor: 'storageSize',
|
||||
category: 'Storage',
|
||||
unitName: 'GB',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.STORAGE_IMAGES_TRANSFORMED,
|
||||
name: 'Storage Image Transformations',
|
||||
units: 'absolute',
|
||||
anchor: 'storageImageTransformations',
|
||||
category: 'Storage',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.REALTIME_PEAK_CONNECTIONS,
|
||||
name: 'Realtime Concurrent Peak Connections',
|
||||
units: 'absolute',
|
||||
anchor: 'realtimePeakConnections',
|
||||
category: 'Realtime',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.REALTIME_MESSAGE_COUNT,
|
||||
name: 'Realtime Messages',
|
||||
units: 'absolute',
|
||||
anchor: 'realtimeMessageCount',
|
||||
category: 'Realtime',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.FUNCTION_INVOCATIONS,
|
||||
name: 'Edge Function Invocations',
|
||||
units: 'absolute',
|
||||
anchor: 'funcInvocations',
|
||||
category: 'Edge Functions',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_SIZE_GB_HOURS_GP3,
|
||||
name: 'Disk Size',
|
||||
anchor: 'diskSize',
|
||||
units: 'absolute',
|
||||
unitName: 'GB-Hrs',
|
||||
category: 'Database',
|
||||
tip: 'Each project gets provisioned with 8 GB of GP3 disk for free. When you get close to the disk size limit, we autoscale your disk by 1.5x. Each GB of provisioned disk size beyond 8 GB incurs a GB-Hr every hour. Each extra GB is billed at $0.125/month ($0.000171/GB-Hr), prorated down to the hour.',
|
||||
docLink: {
|
||||
title: 'Read more',
|
||||
url: `${DOCS_URL}/guides/platform/manage-your-usage/disk-size`,
|
||||
export const BILLING_BREAKDOWN_METRICS = (subscription?: OrgSubscription): Metric[] => {
|
||||
return [
|
||||
{
|
||||
key: PricingMetric.DATABASE_SIZE,
|
||||
name: 'Database Size',
|
||||
units: 'bytes',
|
||||
anchor: 'dbSize',
|
||||
category: 'Database',
|
||||
unitName: 'GB',
|
||||
tip: 'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres. This measurement reflects your current database size at this moment. Free Plan projects are limited to 0.5 GB Database Size each and are monitored across the entire billing period.',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_SIZE_GB_HOURS_IO2,
|
||||
name: 'Disk Size IO2',
|
||||
anchor: 'diskSize',
|
||||
units: 'absolute',
|
||||
unitName: 'GB-Hrs',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_IOPS_IO2,
|
||||
name: 'Disk IOPS IO2',
|
||||
units: 'absolute',
|
||||
unitName: 'IOPS-Hrs',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_IOPS_GP3,
|
||||
name: 'Disk IOPS GP3',
|
||||
units: 'absolute',
|
||||
unitName: 'IOPS-Hrs',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_THROUGHPUT_GP3,
|
||||
name: 'Disk Throughput',
|
||||
units: 'absolute',
|
||||
unitName: 'IOPS-Hrs',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_DRAIN_EVENTS,
|
||||
name: 'Log Drain Events',
|
||||
units: 'absolute',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_INGESTION,
|
||||
name: 'Log Ingestion',
|
||||
units: 'bytes',
|
||||
category: 'Logs',
|
||||
anchor: 'logIngestion',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_QUERYING,
|
||||
name: 'Log Query',
|
||||
units: 'bytes',
|
||||
category: 'Logs',
|
||||
anchor: 'logQuery',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_STORAGE,
|
||||
name: 'Log Storage',
|
||||
units: 'bytes',
|
||||
category: 'Logs',
|
||||
anchor: 'logStorage',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.ACTIVE_COMPUTE_HOURS,
|
||||
name: 'Active Compute Hours',
|
||||
units: 'absolute',
|
||||
category: 'compute',
|
||||
anchor: 'active-compute',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.ETL_COPY_BACKFILL_DATA,
|
||||
name: 'Initial Sync Data Processed',
|
||||
units: 'bytes',
|
||||
unitName: 'GB',
|
||||
category: 'Pipelines',
|
||||
anchor: 'pipeline-initial-sync-data',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.ETL_REPLICATED_DATA,
|
||||
name: 'Ongoing Replication Data Processed',
|
||||
units: 'bytes',
|
||||
unitName: 'GB',
|
||||
category: 'Pipelines',
|
||||
anchor: 'pipeline-ongoing-replication-data',
|
||||
},
|
||||
]
|
||||
{
|
||||
key: PricingMetric.EGRESS,
|
||||
name: 'Egress',
|
||||
units: 'bytes',
|
||||
anchor: 'egress',
|
||||
category: 'Database',
|
||||
unitName: 'GB',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.CACHED_EGRESS,
|
||||
name: 'Cached Egress',
|
||||
units: 'bytes',
|
||||
anchor: 'cachedEgress',
|
||||
category: 'Database',
|
||||
unitName: 'GB',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.MONTHLY_ACTIVE_USERS,
|
||||
name: 'Monthly Active Users',
|
||||
units: 'absolute',
|
||||
anchor: 'mau',
|
||||
category: 'Authentication',
|
||||
unitName: 'MAU',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS,
|
||||
name: 'Monthly Active SSO Users',
|
||||
units: 'absolute',
|
||||
anchor: 'mauSso',
|
||||
category: 'Authentication',
|
||||
unitName: 'MAU',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.MONTHLY_ACTIVE_THIRD_PARTY_USERS,
|
||||
name: 'Monthly Active Third-Party Users',
|
||||
units: 'absolute',
|
||||
category: 'Authentication',
|
||||
unitName: 'MAU',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.STORAGE_SIZE,
|
||||
name: 'Storage Size',
|
||||
units: 'bytes',
|
||||
anchor: 'storageSize',
|
||||
category: 'Storage',
|
||||
unitName: 'GB',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.STORAGE_IMAGES_TRANSFORMED,
|
||||
name: 'Storage Image Transformations',
|
||||
units: 'absolute',
|
||||
anchor: 'storageImageTransformations',
|
||||
category: 'Storage',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.REALTIME_PEAK_CONNECTIONS,
|
||||
name: 'Realtime Concurrent Peak Connections',
|
||||
units: 'absolute',
|
||||
anchor: 'realtimePeakConnections',
|
||||
category: 'Realtime',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.REALTIME_MESSAGE_COUNT,
|
||||
name: 'Realtime Messages',
|
||||
units: 'absolute',
|
||||
anchor: 'realtimeMessageCount',
|
||||
category: 'Realtime',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.FUNCTION_INVOCATIONS,
|
||||
name: 'Edge Function Invocations',
|
||||
units: 'absolute',
|
||||
anchor: 'funcInvocations',
|
||||
category: 'Edge Functions',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_SIZE_GB_HOURS_GP3,
|
||||
name: 'Disk Size',
|
||||
anchor: 'diskSize',
|
||||
units: 'absolute',
|
||||
unitName: 'GB-Hrs',
|
||||
category: 'Database',
|
||||
tip: 'Each project gets provisioned with 8 GB of GP3 disk for free. When you get close to the disk size limit, we autoscale your disk by 1.5x. Each GB of provisioned disk size beyond 8 GB incurs a GB-Hr every hour. Each extra GB is billed at $0.125/month ($0.000171/GB-Hr), prorated down to the hour.',
|
||||
docLink: {
|
||||
title: 'Read more',
|
||||
url: `${DOCS_URL}/guides/platform/manage-your-usage/disk-size`,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_SIZE_GB_HOURS_IO2,
|
||||
name: 'Disk Size IO2',
|
||||
anchor: 'diskSize',
|
||||
units: 'absolute',
|
||||
unitName: 'GB-Hrs',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_IOPS_IO2,
|
||||
name: 'Disk IOPS IO2',
|
||||
units: 'absolute',
|
||||
unitName: 'IOPS-Hrs',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_IOPS_GP3,
|
||||
name: 'Disk IOPS GP3',
|
||||
units: 'absolute',
|
||||
unitName: 'IOPS-Hrs',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.DISK_THROUGHPUT_GP3,
|
||||
name: 'Disk Throughput',
|
||||
units: 'absolute',
|
||||
unitName: 'IOPS-Hrs',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_DRAIN_EVENTS,
|
||||
name: 'Log Drain Events',
|
||||
units: 'absolute',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_INGESTION,
|
||||
name: 'Log Ingestion',
|
||||
units: 'bytes',
|
||||
category: 'Logs',
|
||||
anchor: 'logIngestion',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_QUERYING,
|
||||
name: 'Log Query',
|
||||
units: 'bytes',
|
||||
category: 'Logs',
|
||||
anchor: 'logQuery',
|
||||
tip:
|
||||
subscription?.plan.id !== 'platform'
|
||||
? "Your organization gets a log query allowance that scales with how much log data you ingest. Once exceeded, your logs will be degraded. Exceeding your allowance doesn't rate limit queries, shorten retention, or cut off access yet — the grace period ends at the end of 2026."
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_STORAGE,
|
||||
name: 'Log Storage',
|
||||
units: 'bytes',
|
||||
category: 'Logs',
|
||||
anchor: 'logStorage',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.ACTIVE_COMPUTE_HOURS,
|
||||
name: 'Active Compute Hours',
|
||||
units: 'absolute',
|
||||
category: 'compute',
|
||||
anchor: 'active-compute',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.ETL_COPY_BACKFILL_DATA,
|
||||
name: 'Initial Sync Data Processed',
|
||||
units: 'bytes',
|
||||
unitName: 'GB',
|
||||
category: 'Pipelines',
|
||||
anchor: 'pipeline-initial-sync-data',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.ETL_REPLICATED_DATA,
|
||||
name: 'Ongoing Replication Data Processed',
|
||||
units: 'bytes',
|
||||
unitName: 'GB',
|
||||
category: 'Pipelines',
|
||||
anchor: 'pipeline-ongoing-replication-data',
|
||||
},
|
||||
]
|
||||
}
|
||||
+28
-7
@@ -1,7 +1,7 @@
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
import { cn, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
|
||||
import { Badge, cn, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
|
||||
|
||||
import { billingMetricUnit, formatUsage } from '../helpers'
|
||||
import { Metric, USAGE_APPROACHING_THRESHOLD } from './BillingBreakdown.constants'
|
||||
@@ -47,12 +47,16 @@ export const BillingMetric = ({
|
||||
: usageMeta.usage.toLocaleString() + (metric.unitName ? ` ${metric.unitName}` : '')
|
||||
} else {
|
||||
return metric.units === 'bytes' || metric.units === 'gigabytes'
|
||||
? `${usageMeta.usage.toLocaleString() ?? 0} / ${usageMeta.pricing_free_units ?? 0} GB`
|
||||
? `${usageMeta.usage.toLocaleString() ?? 0} / ${usageMeta.pricing_free_units?.toLocaleString() ?? 0} GB`
|
||||
: `${usageMeta.usage.toLocaleString()} / ${usageMeta.pricing_free_units?.toLocaleString()}` +
|
||||
(metric.unitName ? ` ${metric.unitName}` : '')
|
||||
}
|
||||
}, [usageMeta, relativeToSubscription, metric])
|
||||
|
||||
const isLogMetricOnNonPlatformPlan =
|
||||
(metric.key === PricingMetric.LOG_INGESTION || metric.key === PricingMetric.LOG_QUERYING) &&
|
||||
subscription?.plan.id !== 'platform'
|
||||
|
||||
const sortedProjectAllocations = useMemo(() => {
|
||||
if (!usageMeta || !usageMeta.project_allocations) return []
|
||||
|
||||
@@ -86,8 +90,13 @@ export const BillingMetric = ({
|
||||
{metric.anchor ? (
|
||||
<Link href={`/org/${slug}/usage#${metric.anchor}`} className="block w-full group">
|
||||
<div className="group flex items-center gap-1">
|
||||
<p className="text-sm text-foreground-light group-hover:text-foreground transition cursor-pointer">
|
||||
{metric.name}
|
||||
<p className="text-sm text-foreground-light group-hover:text-foreground transition cursor-pointer items-center">
|
||||
<span>{metric.name}</span>
|
||||
{isLogMetricOnNonPlatformPlan && (
|
||||
<Badge className="ml-2" variant={'warning'}>
|
||||
Upcoming
|
||||
</Badge>
|
||||
)}
|
||||
</p>
|
||||
{usageMeta.available_in_plan && (
|
||||
<span className="text-foreground-muted transition inline-block group-hover:transform group-hover:translate-x-0.5">
|
||||
@@ -97,7 +106,10 @@ export const BillingMetric = ({
|
||||
</div>
|
||||
<span className="text-sm">{usageLabel}</span>
|
||||
{relativeToSubscription && usageMeta.cost && usageMeta.cost > 0 ? (
|
||||
<span className="text-sm" translate="no">
|
||||
<span
|
||||
className={cn('text-sm', isLogMetricOnNonPlatformPlan && 'line-through')}
|
||||
translate="no"
|
||||
>
|
||||
({formatCurrency(usageMeta.cost)})
|
||||
</span>
|
||||
) : usageMeta.available_in_plan &&
|
||||
@@ -201,14 +213,23 @@ export const BillingMetric = ({
|
||||
|
||||
{subscription.usage_billing_enabled === false &&
|
||||
relativeToSubscription &&
|
||||
(isApproachingLimit || isExceededLimit) && (
|
||||
(isApproachingLimit || isExceededLimit) &&
|
||||
(isLogMetricOnNonPlatformPlan ? (
|
||||
<div className="my-2">
|
||||
<p className="text-sm">
|
||||
Enforcement of restrictions will only start after the grace period ends on Dec
|
||||
23, 2026. Reduce your usage, upgrade to a usage-based plan or disable the spend
|
||||
cap to avoid restrictions.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="my-2">
|
||||
<p className="text-sm">
|
||||
Exceeding your plans included usage will lead to restrictions to your project.
|
||||
Upgrade to a usage-based plan or disable the spend cap to avoid restrictions.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
|
||||
{sortedProjectAllocations && sortedProjectAllocations.length > 0 && (
|
||||
<table className="list-disc w-full">
|
||||
|
||||
@@ -21,6 +21,9 @@ export const Restriction = () => {
|
||||
usage?.usages.find(
|
||||
(metric) =>
|
||||
metric.metric !== PricingMetric.DISK_SIZE_GB_HOURS_GP3 &&
|
||||
// Log metrics should not trigger the exceeded limits alert for now (soft rollout)
|
||||
metric.metric !== PricingMetric.LOG_INGESTION &&
|
||||
metric.metric !== PricingMetric.LOG_QUERYING &&
|
||||
!metric.unlimited &&
|
||||
metric.capped &&
|
||||
metric.usage > (metric?.pricing_free_units ?? 0)
|
||||
|
||||
@@ -42,11 +42,18 @@ const OrgLogUsage = ({
|
||||
margin: 20,
|
||||
isLoading: isLoadingOrgDailyStats,
|
||||
},
|
||||
[PricingMetric.LOG_STORAGE]: {
|
||||
data: dailyUsageToDataPoints(orgDailyStats, (metric) => metric === PricingMetric.LOG_STORAGE),
|
||||
margin: 0,
|
||||
isLoading: isLoadingOrgDailyStats,
|
||||
},
|
||||
...(subscription?.plan.id === 'platform'
|
||||
? {
|
||||
[PricingMetric.LOG_STORAGE]: {
|
||||
data: dailyUsageToDataPoints(
|
||||
orgDailyStats,
|
||||
(metric) => metric === PricingMetric.LOG_STORAGE
|
||||
),
|
||||
margin: 0,
|
||||
isLoading: isLoadingOrgDailyStats,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,9 +34,7 @@ const METRICS_TO_HIDE_WITH_NO_USAGE: PricingMetric[] = [
|
||||
PricingMetric.DISK_SIZE_GB_HOURS_GP3,
|
||||
PricingMetric.DISK_SIZE_GB_HOURS_IO2,
|
||||
PricingMetric.DISK_THROUGHPUT_GP3,
|
||||
PricingMetric.LOG_INGESTION,
|
||||
PricingMetric.LOG_STORAGE,
|
||||
PricingMetric.LOG_QUERYING,
|
||||
PricingMetric.ACTIVE_COMPUTE_HOURS,
|
||||
PricingMetric.ETL_PIPELINE,
|
||||
PricingMetric.ETL_REPLICATED_DATA,
|
||||
@@ -82,6 +80,9 @@ export const TotalUsage = ({
|
||||
(usageItem) =>
|
||||
// Filter out compute as compute has no quota and is always being charged for
|
||||
!usageItem.metric.startsWith('COMPUTE_') &&
|
||||
// Log metrics should not trigger the exceeded limits alert for now (soft rollout)
|
||||
usageItem.metric !== PricingMetric.LOG_INGESTION &&
|
||||
usageItem.metric !== PricingMetric.LOG_QUERYING &&
|
||||
!usageItem.unlimited &&
|
||||
usageItem.usage > (usageItem?.pricing_free_units ?? 0)
|
||||
)
|
||||
@@ -90,17 +91,28 @@ export const TotalUsage = ({
|
||||
const sortedBillingMetrics = useMemo(() => {
|
||||
if (!usage) return []
|
||||
|
||||
const breakdownMetrics = BILLING_BREAKDOWN_METRICS.filter((metric) =>
|
||||
usage.usages.some((usage) => usage.metric === metric.key)
|
||||
).filter((metric) => {
|
||||
if (!METRICS_TO_HIDE_WITH_NO_USAGE.includes(metric.key as PricingMetric)) return true
|
||||
const breakdownMetrics = BILLING_BREAKDOWN_METRICS(subscription)
|
||||
.filter((metric) => usage.usages.some((usage) => usage.metric === metric.key))
|
||||
.filter((metric) => {
|
||||
if (!METRICS_TO_HIDE_WITH_NO_USAGE.includes(metric.key as PricingMetric)) return true
|
||||
|
||||
const metricUsage = usage.usages.find((it) => it.metric === metric.key)
|
||||
const metricUsage = usage.usages.find((it) => it.metric === metric.key)
|
||||
|
||||
return metricUsage && metricUsage.usage > 0
|
||||
})
|
||||
return metricUsage && metricUsage.usage > 0
|
||||
})
|
||||
|
||||
const PINNED_METRICS = [PricingMetric.LOG_INGESTION, PricingMetric.LOG_QUERYING]
|
||||
|
||||
return breakdownMetrics.slice().sort((a, b) => {
|
||||
const pinnedIndexA = PINNED_METRICS.indexOf(a.key as PricingMetric)
|
||||
const pinnedIndexB = PINNED_METRICS.indexOf(b.key as PricingMetric)
|
||||
|
||||
if (pinnedIndexA !== -1 || pinnedIndexB !== -1) {
|
||||
if (pinnedIndexA === -1) return 1
|
||||
if (pinnedIndexB === -1) return -1
|
||||
return pinnedIndexA - pinnedIndexB
|
||||
}
|
||||
|
||||
const usageMetaA = usage.usages.find((x) => x.metric === a.key)
|
||||
const usageRatioA =
|
||||
typeof usageMetaA !== 'number'
|
||||
@@ -120,7 +132,7 @@ export const TotalUsage = ({
|
||||
usageRatioB - usageRatioA
|
||||
)
|
||||
})
|
||||
}, [usage])
|
||||
}, [usage, subscription])
|
||||
|
||||
const computeMetrics = (usage?.usages || [])
|
||||
.filter((it) => it.metric.startsWith('COMPUTE'))
|
||||
@@ -199,6 +211,7 @@ export const TotalUsage = ({
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 mt-3 gap-px bg-border">
|
||||
{sortedBillingMetrics.map((metric, i) => {
|
||||
return (
|
||||
|
||||
@@ -230,6 +230,23 @@ export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[
|
||||
],
|
||||
})
|
||||
|
||||
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',
|
||||
@@ -373,34 +390,38 @@ export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[
|
||||
key: PricingMetric.LOG_INGESTION,
|
||||
attributes: [{ key: PricingMetric.LOG_INGESTION.toLowerCase(), color: 'white' }],
|
||||
name: 'Log Ingestion',
|
||||
unit: 'absolute',
|
||||
unit: 'bytes',
|
||||
description:
|
||||
'Total amount of logs ingested across all projects.\nBilling is based on the total amount of logs ingested in Gigabyte.',
|
||||
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: [],
|
||||
links: [
|
||||
{
|
||||
name: 'Log Ingestion',
|
||||
url: `${DOCS_URL}/guides/platform/manage-your-usage/log-ingest`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
anchor: 'logQuery',
|
||||
key: PricingMetric.LOG_QUERYING,
|
||||
attributes: [{ key: PricingMetric.LOG_QUERYING.toLowerCase(), color: 'white' }],
|
||||
name: 'Log Query',
|
||||
unit: 'absolute',
|
||||
unit: 'bytes',
|
||||
description:
|
||||
'Total amount of logs queried across all projects.\nBilling is based on the total amount of logs queried in Gigabyte.',
|
||||
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: [],
|
||||
},
|
||||
{
|
||||
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: [],
|
||||
links: [
|
||||
{
|
||||
name: 'Log Query',
|
||||
url: `${DOCS_URL}/guides/platform/manage-your-usage/log-query`,
|
||||
},
|
||||
],
|
||||
},
|
||||
...logStorageAttributes,
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -355,18 +355,16 @@ export const Usage = () => {
|
||||
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
|
||||
/>
|
||||
|
||||
{subscription?.plan.id === 'platform' && (
|
||||
<OrgLogUsage
|
||||
orgSlug={slug as string}
|
||||
projectRef={selectedProjectRef}
|
||||
subscription={subscription}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
currentBillingCycleSelected={currentBillingCycleSelected}
|
||||
orgDailyStats={orgDailyStats}
|
||||
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
|
||||
/>
|
||||
)}
|
||||
<OrgLogUsage
|
||||
orgSlug={slug as string}
|
||||
projectRef={selectedProjectRef}
|
||||
subscription={subscription}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
currentBillingCycleSelected={currentBillingCycleSelected}
|
||||
orgDailyStats={orgDailyStats}
|
||||
isLoadingOrgDailyStats={isLoadingOrgDailyStats || isLoadingSubscription}
|
||||
/>
|
||||
|
||||
<Pipelines
|
||||
orgSlug={slug as string}
|
||||
|
||||
+1
-1
@@ -172,7 +172,7 @@ const AttributeUsage = ({
|
||||
) : (
|
||||
<p className="text-xs">
|
||||
{attribute.unit === 'bytes' || attribute.unit === 'gigabytes'
|
||||
? `${usageMeta.pricing_free_units ?? 0} GB`
|
||||
? `${usageMeta.pricing_free_units?.toLocaleString() ?? 0} GB`
|
||||
: (usageMeta.pricing_free_units ?? 0).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useFlag } from 'common'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { cn } from 'ui'
|
||||
@@ -44,6 +43,12 @@ const METRICS: MetricConfig[] = [
|
||||
unit: 'gigabytes',
|
||||
anchor: 'logIngestion',
|
||||
},
|
||||
{
|
||||
key: PricingMetric.LOG_QUERYING,
|
||||
label: 'Log Query',
|
||||
unit: 'gigabytes',
|
||||
anchor: 'logQuery',
|
||||
},
|
||||
]
|
||||
|
||||
const formatCount = (value: number) => value.toLocaleString()
|
||||
@@ -175,22 +180,14 @@ export const PlanUsageCard = () => {
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const { data: usage, isSuccess, isError } = useOrgUsageQuery({ orgSlug: organization?.slug })
|
||||
|
||||
const logIngestionBillingEnabled = useFlag('logIngestionBillingEnabled')
|
||||
|
||||
const metricsIncludingLogIngestion = logIngestionBillingEnabled
|
||||
? METRICS
|
||||
: METRICS.filter((m) => m.key !== PricingMetric.LOG_INGESTION)
|
||||
|
||||
const visibleRows = isSuccess
|
||||
? metricsIncludingLogIngestion
|
||||
.map((config) => {
|
||||
const usageItem = usage.usages.find((u) => u.metric === config.key)
|
||||
if (!usageItem) return null
|
||||
if (!usageItem.available_in_plan) return null
|
||||
if (!usageItem.pricing_free_units || usageItem.pricing_free_units <= 0) return null
|
||||
return { config, usageItem }
|
||||
})
|
||||
.filter((row): row is { config: MetricConfig; usageItem: OrgMetricsUsage } => row !== null)
|
||||
? METRICS.map((config) => {
|
||||
const usageItem = usage.usages.find((u) => u.metric === config.key)
|
||||
if (!usageItem) return null
|
||||
if (!usageItem.available_in_plan) return null
|
||||
if (!usageItem.pricing_free_units || usageItem.pricing_free_units <= 0) return null
|
||||
return { config, usageItem }
|
||||
}).filter((row): row is { config: MetricConfig; usageItem: OrgMetricsUsage } => row !== null)
|
||||
: []
|
||||
|
||||
if (isError) return null
|
||||
@@ -223,9 +220,7 @@ export const PlanUsageCard = () => {
|
||||
orgSlug={organization?.slug ?? '_'}
|
||||
/>
|
||||
))
|
||||
: metricsIncludingLogIngestion.map((config) => (
|
||||
<SkeletonMetricRow key={config.key} label={config.label} />
|
||||
))}
|
||||
: METRICS.map((config) => <SkeletonMetricRow key={config.key} label={config.label} />)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -9,7 +9,10 @@ export const getResourcesExceededLimitsOrg = (usageMetrics: OrgMetricsUsage[]):
|
||||
!usageMetric.capped ||
|
||||
!usageMetric.available_in_plan ||
|
||||
usageMetric.unlimited ||
|
||||
usageMetric.metric === 'DISK_IOPS_GP3'
|
||||
usageMetric.metric === 'DISK_IOPS_GP3' ||
|
||||
// Log metrics should not trigger the exceeded limits alert for now (soft rollout)
|
||||
usageMetric.metric === 'LOG_INGESTION' ||
|
||||
usageMetric.metric === 'LOG_QUERYING'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -42,18 +42,20 @@ const ProjectsPage: NextPageWithLayout = () => {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-4 xl:flex-row xl:gap-x-6">
|
||||
<div className="flex flex-col gap-y-4 flex-1 min-w-0">
|
||||
<HomePageActions />
|
||||
<ProjectList />
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col gap-y-4 xl:flex-row xl:gap-x-6">
|
||||
<div className="flex flex-col gap-y-4 flex-1 min-w-0">
|
||||
<HomePageActions />
|
||||
<ProjectList />
|
||||
</div>
|
||||
{showOrgProjectsListUsageCard && (
|
||||
<aside className="xl:w-80 xl:shrink-0">
|
||||
<ul className="list-none p-0 m-0">
|
||||
<PlanUsageCard />
|
||||
</ul>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
{showOrgProjectsListUsageCard && (
|
||||
<aside className="xl:w-80 xl:shrink-0">
|
||||
<ul className="list-none p-0 m-0">
|
||||
<PlanUsageCard />
|
||||
</ul>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ScaffoldSection>
|
||||
|
||||
@@ -4,6 +4,7 @@ export const LOCAL_STORAGE_KEYS = {
|
||||
/**
|
||||
* STUDIO
|
||||
*/
|
||||
LOG_PRICING_ANNOUNCEMENT_DISMISSED: 'log-pricing-announcement-dismissed',
|
||||
AI_ASSISTANT_STATE: (projectRef: string | undefined) =>
|
||||
`supabase-ai-assistant-state-${projectRef}`,
|
||||
SIDEBAR_BEHAVIOR: 'supabase-sidebar-behavior',
|
||||
|
||||
Reference in new issue
Block a user