import {Fragment, useEffect} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import type {Client} from 'sentry/api'; import ErrorBoundary from 'sentry/components/errorBoundary'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {DATA_CATEGORY_INFO} from 'sentry/constants'; import {space} from 'sentry/styles/space'; import {DataCategory} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import {useApiQuery} from 'sentry/utils/queryClient'; import withApi from 'sentry/utils/withApi'; import withOrganization from 'sentry/utils/withOrganization'; import {openCodecovModal} from 'getsentry/actionCreators/modal'; import withSubscription from 'getsentry/components/withSubscription'; import type { BillingStatTotal, CustomerUsage, Plan, ProductTrial, PromotionData, ReservedBudgetForCategory, Subscription, } from 'getsentry/types'; import {PlanTier} from 'getsentry/types'; import {hasAccessToSubscriptionOverview, isAm3DsPlan} from 'getsentry/utils/billing'; import {sortCategories} from 'getsentry/utils/dataCategory'; import withPromotions from 'getsentry/utils/withPromotions'; import ContactBillingMembers from 'getsentry/views/contactBillingMembers'; import {openOnDemandBudgetEditModal} from 'getsentry/views/onDemandBudgets/editOnDemandButton'; import openPerformanceQuotaCreditsPromoModal from './promotions/performanceQuotaCreditsPromo'; import openPerformanceReservedTransactionsDiscountModal from './promotions/performanceReservedTransactionsPromo'; import TrialEnded from './trial/trialEnded'; import OnDemandDisabled from './ondemandDisabled'; import {OnDemandSettings} from './onDemandSettings'; import {DisplayModeToggle} from './overviewDisplayModeToggle'; import RecurringCredits from './recurringCredits'; import ReservedUsageChart from './reservedUsageChart'; import SubscriptionHeader from './subscriptionHeader'; import UsageAlert from './usageAlert'; import UsageTotals from './usageTotals'; import {trackSubscriptionView} from './utils'; type Props = { api: Client; location: Location; organization: Organization; promotionData: PromotionData; subscription: Subscription; }; /** * Subscription overview page. */ function Overview({api, location, subscription, organization, promotionData}: Props) { const displayMode = ['cost', 'usage'].includes(location.query.displayMode as string) ? (location.query.displayMode as 'cost' | 'usage') : 'usage'; const hasBillingPerms = organization.access?.includes('org:billing'); // we fetch an expanded view of the subscription which includes usage // data for the current period const { data: usage, refetch: refetchUsage, isPending, isError, } = useApiQuery([`/customers/${organization.slug}/usage/`], { staleTime: 60_000, }); const reservedBudgetCategoryInfo: Record = {}; subscription.reservedBudgets?.forEach(rb => { Object.entries(rb.categories).forEach(([category, rbmh]) => { reservedBudgetCategoryInfo[category] = { freeBudget: rb.freeBudget, totalReservedBudget: rb.reservedBudget, reservedSpend: rbmh.reservedSpend, reservedCpe: rbmh.reservedCpe, prepaidBudget: rb.reservedBudget + rb.freeBudget, }; }); }); useEffect(() => { if (promotionData) { let promotion = promotionData.availablePromotions?.find( promo => promo.promptActivityTrigger === 'performance_reserved_txns_discount_v1' ); if (promotion) { openPerformanceReservedTransactionsDiscountModal({ api, promotionData, organization, promptFeature: 'performance_reserved_txns_discount_v1', }); return; } promotion = promotionData.availablePromotions?.find( promo => promo.promptActivityTrigger === 'performance_quota_credits_v1' ); if (promotion) { openPerformanceQuotaCreditsPromoModal({api, promotionData, organization}); return; } promotion = promotionData.availablePromotions?.find( promo => promo.promptActivityTrigger === 'performance_reserved_txns_discount' ); if (promotion) { openPerformanceReservedTransactionsDiscountModal({ api, promotionData, organization, promptFeature: 'performance_reserved_txns_discount', }); return; } } // open the codecov modal if the query param is present if ( location.query?.open_codecov_modal === '1' && // self serve or has billing perms can view it hasAccessToSubscriptionOverview(subscription, organization) ) { openCodecovModal({organization}); } // Open on-demand budget modal if hash fragment present and user has access if ( window.location.hash === '#open-ondemand-modal' && subscription.supportsOnDemand && hasAccessToSubscriptionOverview(subscription, organization) ) { openOnDemandBudgetEditModal({organization, subscription}); // Clear hash to prevent modal reopening on refresh window.history.replaceState( null, '', window.location.pathname + window.location.search ); } }, [organization, location.query, subscription, promotionData, api]); useEffect( () => void trackSubscriptionView(organization, subscription, 'overview'), [subscription, organization] ); // Sales managed accounts do not allow members to view the billing page. // Whilst self-serve accounts do. if (!hasBillingPerms && !subscription.canSelfServe) { return ; } function renderUsageChart(usageData: CustomerUsage) { const {stats, periodStart, periodEnd} = usageData; return ( ); } function renderUsageCards(usageData: CustomerUsage) { const nonPlanProductTrials: ProductTrial[] = subscription.productTrials?.filter( pt => !Object.keys(subscription.categories).includes(pt.category) ) || []; const showProductTrialEventBreakdown: boolean = nonPlanProductTrials?.filter(pt => pt.category === DataCategory.PROFILES).length > 0 || false; if ( !subscription.hadCustomDynamicSampling && isAm3DsPlan(subscription.plan) && !subscription.isEnterpriseTrial ) { // if the customer has not yet used custom DS in the current period, just show // one spans card reservedBudgetCategoryInfo[DataCategory.SPANS]!.reservedSpend += reservedBudgetCategoryInfo[DataCategory.SPANS_INDEXED]!.reservedSpend ?? 0; } return ( {sortCategories(subscription.categories).map(categoryHistory => { const category = categoryHistory.category; if ( category === DATA_CATEGORY_INFO.spanIndexed.plural && !subscription.hadCustomDynamicSampling ) { return null; } // The usageData does not include details for seat-based categories. // For now we will handle the monitor category specially let monitor_usage: number | undefined = 0; if (category === DataCategory.MONITOR_SEATS) { monitor_usage = subscription.categories.monitorSeats?.usage; } if (category === DataCategory.UPTIME) { monitor_usage = subscription.categories.uptime?.usage; } const categoryTotals: BillingStatTotal = category !== DataCategory.MONITOR_SEATS && category !== DataCategory.UPTIME ? usageData.totals[category]! : { accepted: monitor_usage ?? 0, dropped: 0, droppedOther: 0, droppedOverQuota: 0, droppedSpikeProtection: 0, filtered: 0, projected: 0, }; const eventTotals = category !== DataCategory.MONITOR_SEATS && category !== DataCategory.UPTIME ? usageData.eventTotals?.[category] : undefined; const showEventBreakdown = organization.features.includes('profiling-billing') && subscription.planTier === PlanTier.AM2; return ( ); })} {nonPlanProductTrials?.map(pt => { const categoryTotals = usageData.totals[pt.category]; const eventTotals = usageData.eventTotals?.[pt.category]; return ( ); })} ); } if (isPending) { return ( ); } if (isError) { return ; } /** * It's important to separate the views for folks with billing permissions (org:billing) and those without. * Only owners and billing admins have the billing scope, everyone else including managers, admins, and members lack that scope. * * Non-billing users should be able to see the following info: * - Current Plan information and the date when it ends * - Event totals, dropped events, usage charts * - Alerts for overages (usage alert, grace period, etc) * - CTAs asking the user to request a plan change * * Non-billing users should NOT see any of the following: * - Anything with a dollar amount * - Receipts * - Credit card on file * - Previous usage history * - On-demand information */ function contentWithBillingPerms(usageData: CustomerUsage, planDetails: Plan) { return ( {renderUsageChart(usageData)} {renderUsageCards(usageData)} ); } function contentWithoutBillingPerms(usageData: CustomerUsage) { return ( {renderUsageChart(usageData)} {renderUsageCards(usageData)} ); } return (
{hasBillingPerms ? contentWithBillingPerms(usage, subscription.planDetails) : contentWithoutBillingPerms(usage)}
); } export default withApi(withOrganization(withSubscription(withPromotions(Overview)))); const TotalsWrapper = styled('div')` margin-bottom: ${space(3)}; `;