import { useQuery } from "@tanstack/react-query"; import { AUTUMN_SEO_DATA_BALANCE_FEATURE_ID, AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID, autumnSeoDataCreditsToUsd, } from "@/shared/billing"; import { creditFeatureLabel, mapDataforseoPathToCreditFeature, } from "@/shared/billing-credit-features"; import { getBillingUsageEvents, type BillingUsageEvent, } from "@/serverFunctions/billing"; const BILLING_USAGE_FEATURE_IDS: string[] = [ AUTUMN_SEO_DATA_BALANCE_FEATURE_ID, AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID, ]; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; type BillingUsageEventProperties = { creditFeature?: unknown; credit_feature?: unknown; path?: unknown; paths?: unknown; }; type BillingFeatureBreakdownRow = { label: string; usd: number; }; type BillingUsageRange = { start: number; end: number; }; function getLast30DayUsageRange(): BillingUsageRange { const end = Date.now(); return { start: end - THIRTY_DAYS_MS, end, }; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function getPathSegmentsFromProperties( properties: BillingUsageEventProperties, ): string[] | null { const paths = properties.paths ?? properties.path; if (Array.isArray(paths)) { const stringPaths = paths.filter( (value): value is string => typeof value === "string", ); if ( stringPaths.length > 1 && stringPaths.every((segment) => !segment.includes("/")) ) { return stringPaths; } const path = stringPaths[0]; if (!path) return null; const parsedPath = parseJsonEncodedPath(path); return parsedPath ?? path.split("/").filter(Boolean); } if (typeof paths !== "string") return null; const parsedPath = parseJsonEncodedPath(paths); return parsedPath ?? paths.split("/").filter(Boolean); } function parseJsonEncodedPath(path: string): string[] | null { if (!path.startsWith("[")) return null; try { const parsed: unknown = JSON.parse(path); if (!Array.isArray(parsed)) return null; const stringPaths = parsed.filter( (value): value is string => typeof value === "string", ); if ( stringPaths.length > 1 && stringPaths.every((segment) => !segment.includes("/")) ) { return stringPaths; } const firstPath = stringPaths[0]; return firstPath ? firstPath.split("/").filter(Boolean) : null; } catch { return null; } } function getCreditFeatureFromUsageEvent( event: BillingUsageEvent, ): string | null { const properties = isRecord(event.properties) ? event.properties : {}; const explicitFeature = properties.creditFeature ?? properties.credit_feature; if (typeof explicitFeature === "string" && explicitFeature.length > 0) { return explicitFeature; } const path = getPathSegmentsFromProperties(properties); return path ? mapDataforseoPathToCreditFeature(path) : null; } export function getBillingFeatureBreakdownRows( events: BillingUsageEvent[], ): BillingFeatureBreakdownRow[] { const creditsByLabel = new Map(); for (const event of events) { const feature = getCreditFeatureFromUsageEvent(event); const label = feature ? creditFeatureLabel(feature) : "Other"; creditsByLabel.set(label, (creditsByLabel.get(label) ?? 0) + event.value); } return [...creditsByLabel.entries()] .map(([label, credits]) => ({ label, usd: autumnSeoDataCreditsToUsd(credits), })) .filter((row) => row.usd > 0) .toSorted((a, b) => b.usd - a.usd); } export function BillingFeatureBreakdown() { const eventsQuery = useQuery({ queryKey: ["billing", "usage-events", BILLING_USAGE_FEATURE_IDS, "30d"], queryFn: () => getBillingUsageEvents({ data: getLast30DayUsageRange() }), staleTime: 60_000, }); const rows = getBillingFeatureBreakdownRows(eventsQuery.data ?? []); const total = rows.reduce((sum, row) => sum + row.usd, 0); return (
Usage by feature Last 30 days
{eventsQuery.isLoading ? (
{[0, 1, 2, 3].map((i) => (
))}
) : rows.length === 0 ? (
No usage recorded yet
) : (
    {rows.map((row) => (
  • {row.label} ${row.usd.toFixed(2)}
  • ))}
)}
); }