import styled from '@emotion/styled'; import moment from 'moment-timezone'; import Panel from 'sentry/components/panels/panel'; import {IconFire, IconStats, IconWarning} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {DataCategory} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import oxfordizeArray from 'sentry/utils/oxfordizeArray'; import withOrganization from 'sentry/utils/withOrganization'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; import AddEventsCTA from 'getsentry/components/addEventsCTA'; import {GIGABYTE, RESERVED_BUDGET_QUOTA} from 'getsentry/constants'; import OrgStatsBanner from 'getsentry/hooks/orgStatsBanner'; import type {CustomerUsage, Subscription} from 'getsentry/types'; import { formatReservedWithUnits, formatUsageWithUnits, getBestActionToIncreaseEventLimits, hasPerformance, isBizPlanFamily, isUnlimitedReserved, UsageAction, } from 'getsentry/utils/billing'; import {getPlanCategoryName, sortCategoriesWithKeys} from 'getsentry/utils/dataCategory'; import {ButtonWrapper, SubscriptionBody} from './styles'; type ProjectedOverages = string[]; type Props = { organization: Organization; subscription: Subscription; usage: CustomerUsage; }; function UsageAlert({organization, subscription, usage}: Props) { function getActionSentence() { switch (getBestActionToIncreaseEventLimits(organization, subscription)) { case UsageAction.START_TRIAL: return t('Start a free trial to avoid data loss.'); case UsageAction.ADD_EVENTS: return t('Increase your event limits to avoid data loss.'); case UsageAction.REQUEST_ADD_EVENTS: return t( 'Bug your organization owner to increase your event limits to avoid data loss.' ); case UsageAction.REQUEST_UPGRADE: return t('Bug your organization owner to upgrade your plan to avoid data loss.'); case UsageAction.SEND_TO_CHECKOUT: default: return t('Upgrade your plan to avoid data loss.'); } } function formatProjected(projected: number, category: string): string { const displayName = getPlanCategoryName({ plan: subscription.planDetails, category, capitalize: false, hadCustomDynamicSampling: subscription.hadCustomDynamicSampling, }); return category === DataCategory.ATTACHMENTS ? `${formatUsageWithUnits(projected, category)} of attachments` : `${formatReservedWithUnits(projected, category, { isAbbreviated: true, })} ${displayName}`; } function projectedCategoryOverages() { // hide projected overages for plans with on-demand for now since // shared on-demand budget can be applied to any data category if (subscription.onDemandMaxSpend) { return []; } return Object.entries(subscription.categories).reduce( (acc, [category, currentHistory]) => { if ( currentHistory.reserved === RESERVED_BUDGET_QUOTA || isUnlimitedReserved(currentHistory.reserved) ) { return acc; } const projected = usage.totals[category]?.projected || 0; const projectedWithReservedUnit = category === DataCategory.ATTACHMENTS ? projected / GIGABYTE : projected; const hasOverage = !!currentHistory.reserved && projectedWithReservedUnit > (currentHistory.prepaid ?? 0); if (hasOverage) { acc.push(formatProjected(projected, category)); } return acc; }, [] ); } function getProjectedOverages(): ProjectedOverages { if (subscription.isEnterpriseTrial || subscription.hasOverageNotificationsDisabled) { return []; } return projectedCategoryOverages(); } function renderProjectedInfo(projectedOverages: ProjectedOverages) { if (!projectedOverages) { return null; } return (

{t('Projected Overage')}

{tct( `Based on your previous usage, we predict your organization will need at least [totals].`, {totals: oxfordizeArray(projectedOverages)} )}{' '} {getActionSentence()}
{renderPrimaryCTA('projected-overage')}
); } function renderGracePeriodInfo() { return (

{t('Grace Period')}

{tct( `Your organization has depleted its error capacity for the current usage period. We've put your account into a one time grace period, which will continue to accept errors at a limited rate. This grace period ends on [gracePeriodEnd].`, {gracePeriodEnd: moment(subscription.gracePeriodEnd).format('ll')} )}{' '} {getActionSentence()}
{renderPrimaryCTA('grace-period')}
); } function renderExceededInfo() { const exceededList = sortCategoriesWithKeys(subscription.categories) .filter( ([category]) => category !== DataCategory.SPANS_INDEXED || subscription.hadCustomDynamicSampling ) .reduce((acc, [category, currentHistory]) => { if (currentHistory.usageExceeded) { acc.push( getPlanCategoryName({ plan: subscription.planDetails, category, capitalize: false, hadCustomDynamicSampling: subscription.hadCustomDynamicSampling, }) ); } return acc; }, [] as string[]); const quotasExceeded = exceededList.length > 0 ? oxfordizeArray(exceededList) : getPlanCategoryName({ plan: subscription.planDetails, category: DataCategory.ERRORS, capitalize: false, hadCustomDynamicSampling: subscription.hadCustomDynamicSampling, }); return (

{t('Usage Exceeded')}

{tct( `Your organization has depleted its [quotasExceeded] capacity for the current usage period.`, {quotasExceeded} )}{' '} {getActionSentence()}
{renderPrimaryCTA('exceded-quota')}
); } function renderDefaultEventCTA() { // allow business plan members to request events even if no overages // every other user will have another type of CTA if ( getBestActionToIncreaseEventLimits(organization, subscription) === 'request_add_events' && isBizPlanFamily(subscription.planDetails) && hasPerformance(subscription.planDetails) ) { return ( ); } return null; } function renderPrimaryCTA(alertType: string) { if (!subscription.canSelfServe) { return null; } return ( ); } if (!subscription || !usage) { return null; } const hasExceeded = Object.values(subscription.categories).some(({usageExceeded}) => usageExceeded) || // TODO: Remove when mmx plans have error BillingMetricHistory subscription.usageExceeded; const projectedOverages = getProjectedOverages(); const hasOverage = subscription.isGracePeriod || hasExceeded || !!projectedOverages.length; // if no overage, we can still have a CTA if (!hasOverage) { return renderDefaultEventCTA(); } const showProjected = !hasExceeded && !subscription.isGracePeriod; return (
{hasExceeded && renderExceededInfo()} {subscription.isGracePeriod && renderGracePeriodInfo()} {showProjected && renderProjectedInfo(projectedOverages)}
); } export default withOrganization(UsageAlert); const UsageInfo = styled('div')` display: grid; grid-template-columns: max-content auto; gap: ${space(1)}; `; const Description = styled(TextBlock)` font-size: ${p => p.theme.fontSizeMedium}; color: ${p => p.theme.subText}; margin-bottom: 0; `;