Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ecf4694
feat(usage overview): Introduce util functions and hook
isabellaenriquez Nov 28, 2025
a9fdc2d
fix test + knip
isabellaenriquez Nov 28, 2025
28f2d3e
add this too
isabellaenriquez Nov 28, 2025
6b0f5aa
feat(usage overview): Update buttons
isabellaenriquez Nov 28, 2025
a6efc5d
update tests + tweak style
isabellaenriquez Nov 28, 2025
f7623fe
also add this
isabellaenriquez Nov 28, 2025
485be78
feat(usage overview): Extract drawer charts into component
isabellaenriquez Nov 28, 2025
145d051
rm unused exported type
isabellaenriquez Nov 28, 2025
ab127d3
update tests
isabellaenriquez Nov 28, 2025
87a9f70
feat(usage overview): Introduce new panel
isabellaenriquez Nov 28, 2025
eec3fb6
split out breakdownInfo
isabellaenriquez Nov 28, 2025
bd9804f
feat(usage overview): Add breakdown into panel
isabellaenriquez Nov 28, 2025
eb07afa
fix types
isabellaenriquez Nov 28, 2025
6d701b6
add padding
isabellaenriquez Nov 28, 2025
f8b12e6
feat(usage overview): Introduce new table
isabellaenriquez Dec 1, 2025
800b057
feat(usage overview): Release new Usage Overview
isabellaenriquez Dec 1, 2025
eb1e712
fix tests
isabellaenriquez Dec 1, 2025
39e1d46
Merge branch 'isabella/uo-v2-pt-6' into isabella/uo-v2-pt-7
isabellaenriquez Dec 1, 2025
99543b4
fix mocks
isabellaenriquez Dec 1, 2025
ad8f5ac
Merge branch 'master' into isabella/uo-v2-pt-7
isabellaenriquez Dec 2, 2025
1794e53
small cosmetic tweaks + hide irrelevant breakdown info
isabellaenriquez Dec 3, 2025
8839526
add lock to header
isabellaenriquez Dec 3, 2025
b9ea509
alter cta
isabellaenriquez Dec 4, 2025
8361ee1
fix translation + inner table bug
isabellaenriquez Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions static/gsApp/utils/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,23 @@ export function supportsPayg(subscription: Subscription) {
return subscription.planDetails.allowOnDemand && subscription.supportsOnDemand;
}

/**
* Whether the category can use PAYG on the subscription given existing budgets.
* Does not check if there is PAYG left.
*/
export function hasPaygBudgetForCategory(
subscription: Subscription,
category: DataCategory
) {
if (!subscription.onDemandBudgets) {
return false;
}
if (subscription.onDemandBudgets.budgetMode === OnDemandBudgetMode.PER_CATEGORY) {
return (subscription.onDemandBudgets.budgets?.[category] ?? 0) > 0;
}
return subscription.onDemandBudgets.sharedMaxBudget > 0;
}

/**
* Returns true if the current user has billing perms.
*/
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {DataCategory} from 'sentry/types/core';
import {defined} from 'sentry/utils';
import {toTitleCase} from 'sentry/utils/string/toTitleCase';

import {UNLIMITED, UNLIMITED_RESERVED} from 'getsentry/constants';
import {UNLIMITED_RESERVED} from 'getsentry/constants';
import type {
BillingMetricHistory,
Plan,
Expand All @@ -21,6 +21,7 @@ import {
displayBudgetName,
formatReservedWithUnits,
getSoftCapType,
hasPaygBudgetForCategory,
isTrialPlan,
supportsPayg,
} from 'getsentry/utils/billing';
Expand Down Expand Up @@ -96,21 +97,25 @@ function UsageBreakdownInfo({
!!formattedAdditionalReserved ||
!!formattedGifted;
const shouldShowReservedSpend =
defined(recurringReservedSpend) && subscription.canSelfServe;
defined(recurringReservedSpend) &&
recurringReservedSpend > 0 &&
subscription.canSelfServe;
const shouldShowAdditionalSpend =
shouldShowReservedSpend || canUsePayg || defined(formattedSoftCapType);

if (!shouldShowIncludedVolume && !shouldShowAdditionalSpend) {
return null;
}

const interval = plan.contractInterval === 'monthly' ? t('month') : t('year');

return (
<Grid columns="repeat(2, 1fr)" gap="md lg" padding="xl">
{shouldShowIncludedVolume && (
<Flex direction="column" gap="lg">
<Text bold>{t('Included volume')}</Text>
{activeProductTrial && (
<UsageBreakdownField field={t('Trial')} value={UNLIMITED} />
<UsageBreakdownField field={t('Trial')} value={t('Unlimited')} />
)}
{formattedPlatformReserved && (
<UsageBreakdownField
Expand Down Expand Up @@ -165,7 +170,7 @@ function UsageBreakdownInfo({
{shouldShowReservedSpend && (
<UsageBreakdownField
field={t('Reserved spend')}
value={displayPriceWithCents({cents: recurringReservedSpend})}
value={`${displayPriceWithCents({cents: recurringReservedSpend})} / ${interval}`}
help={t(
'The amount you spend on additional reserved volume for this product per billing cycle.'
)}
Expand All @@ -184,7 +189,9 @@ function DataCategoryUsageBreakdownInfo({
activeProductTrial,
}: DataCategoryUsageBreakdownInfoProps) {
const {planDetails: plan} = subscription;
const productCanUsePayg = plan.onDemandCategories.includes(category);
const productCanUsePayg =
plan.onDemandCategories.includes(category) &&
hasPaygBudgetForCategory(subscription, category);
const platformReserved =
plan.planCategories[category]?.find(
bucket => bucket.price === 0 && bucket.events >= 0
Expand All @@ -198,23 +205,26 @@ function DataCategoryUsageBreakdownInfo({
);
const isAddOnChildCategory = addOnDataCategories.includes(category) && !isUnlimited;

const shouldShowAdditionalReserved =
!isAddOnChildCategory && !isUnlimited && subscription.canSelfServe;
const formattedPlatformReserved = isAddOnChildCategory
? null
: formatReservedWithUnits(
shouldShowAdditionalReserved ? platformReserved : reserved,
category
);
const additionalReserved = Math.max(0, reserved - platformReserved);
const shouldShowAdditionalReserved =
!isAddOnChildCategory &&
!isUnlimited &&
subscription.canSelfServe &&
additionalReserved > 0;
const formattedAdditionalReserved = shouldShowAdditionalReserved
? formatReservedWithUnits(additionalReserved, category)
: null;
const formattedPlatformReserved =
isAddOnChildCategory || !reserved
? null
: formatReservedWithUnits(
shouldShowAdditionalReserved ? platformReserved : reserved,
category
);

const gifted = metricHistory.free ?? 0;
const formattedGifted = isAddOnChildCategory
? null
: formatReservedWithUnits(gifted, category);
const formattedGifted =
isAddOnChildCategory || !gifted ? null : formatReservedWithUnits(gifted, category);

const paygSpend = metricHistory.onDemandSpendUsed ?? 0;
const paygCategoryBudget = metricHistory.onDemandBudget ?? 0;
Expand Down Expand Up @@ -248,8 +258,10 @@ function ReservedBudgetUsageBreakdownInfo({
activeProductTrial,
}: ReservedBudgetUsageBreakdownInfoProps) {
const {planDetails: plan, categories: metricHistories} = subscription;
const productCanUsePayg = reservedBudget.dataCategories.every(category =>
plan.onDemandCategories.includes(category)
const productCanUsePayg = reservedBudget.dataCategories.every(
category =>
plan.onDemandCategories.includes(category) &&
hasPaygBudgetForCategory(subscription, category)
);
const onTrialOrSponsored = isTrialPlan(subscription.plan) || subscription.isSponsored;

Expand All @@ -265,7 +277,10 @@ function ReservedBudgetUsageBreakdownInfo({
});

const formattedAdditionalReserved = null;
const formattedGifted = displayPriceWithCents({cents: reservedBudget.freeBudget});
const formattedGifted =
reservedBudget.freeBudget > 0
? displayPriceWithCents({cents: reservedBudget.freeBudget})
: null;
const paygSpend = reservedBudget.dataCategories.reduce((acc, category) => {
return acc + (metricHistories[category]?.onDemandSpendUsed ?? 0);
}, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,4 @@ function UsageCharts({
</Container>
);
}

export default UsageCharts;
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('ProductBreakdownPanel', () => {
expect(screen.getByText('Pay-as-you-go')).toBeInTheDocument();
expect(screen.getByText('$10.00')).toBeInTheDocument();
expect(screen.getByText('Reserved spend')).toBeInTheDocument();
expect(screen.getByText('$245.00')).toBeInTheDocument();
expect(screen.getByText('$245.00 / month')).toBeInTheDocument();
});

it('renders for data category with per-category PAYG set', async () => {
Expand Down Expand Up @@ -111,13 +111,19 @@ describe('ProductBreakdownPanel', () => {
expect(screen.getByText('$10.00 /')).toBeInTheDocument();
expect(screen.getByText('$100.00')).toBeInTheDocument(); // shows per-category individual budget
expect(screen.getByText('Reserved spend')).toBeInTheDocument();
expect(screen.getByText('$245.00')).toBeInTheDocument();
expect(screen.getByText('$245.00 / month')).toBeInTheDocument();
});

it('renders for reserved budget add-on', async () => {
const legacySeerSubscription = SubscriptionWithLegacySeerFixture({
organization,
plan: 'am3_business',
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 100_00,
onDemandSpendUsed: 0,
},
});
legacySeerSubscription.reservedBudgets![0] = {
...legacySeerSubscription.reservedBudgets![0]!,
Expand Down Expand Up @@ -153,13 +159,19 @@ describe('ProductBreakdownPanel', () => {
expect(screen.getByText('Pay-as-you-go')).toBeInTheDocument();
expect(screen.getByText('$2.00')).toBeInTheDocument();
expect(screen.getByText('Reserved spend')).toBeInTheDocument();
expect(screen.getByText('$20.00')).toBeInTheDocument();
expect(screen.getByText('$20.00 / month')).toBeInTheDocument();
});

it('renders for reserved budget add-on data category', async () => {
const legacySeerSubscription = SubscriptionWithLegacySeerFixture({
organization,
plan: 'am3_business',
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 100_00,
onDemandSpendUsed: 0,
},
});
legacySeerSubscription.reservedBudgets![0] = {
...legacySeerSubscription.reservedBudgets![0]!,
Expand Down Expand Up @@ -300,4 +312,24 @@ describe('ProductBreakdownPanel', () => {
await screen.findByRole('heading', {name: 'Errors'});
expect(screen.getByText('Pay-as-you-go limit reached')).toBeInTheDocument();
});

it('hides irrelevant breakdown fields', async () => {
render(
<ProductBreakdownPanel
subscription={subscription}
organization={organization}
usageData={usageData}
selectedProduct={DataCategory.ERRORS}
/>
);
await screen.findByRole('heading', {name: 'Errors'});
expect(screen.getByText('Included volume')).toBeInTheDocument();
expect(screen.getByText('Business plan')).toBeInTheDocument();
expect(screen.getByText('50,000')).toBeInTheDocument();
expect(screen.queryByText('Additional reserved')).not.toBeInTheDocument();
expect(screen.queryByText('Gifted')).not.toBeInTheDocument();
expect(screen.queryByText('Additional spend')).not.toBeInTheDocument();
expect(screen.queryByText('Pay-as-you-go')).not.toBeInTheDocument();
expect(screen.queryByText('Reserved spend')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ import {
ProductTrialCta,
UpgradeCta,
} from 'getsentry/views/subscriptionPage/usageOverview/components/upgradeOrTrialCta';
import {USAGE_OVERVIEW_PANEL_HEADER_HEIGHT} from 'getsentry/views/subscriptionPage/usageOverview/constants';
import type {BreakdownPanelProps} from 'getsentry/views/subscriptionPage/usageOverview/types';

function PanelHeader({
selectedProduct,
subscription,
}: Pick<BreakdownPanelProps, 'selectedProduct' | 'subscription'>) {
isInline,
}: Pick<BreakdownPanelProps, 'selectedProduct' | 'subscription' | 'isInline'>) {
const {onDemandBudgets: paygBudgets} = subscription;

const {
Expand Down Expand Up @@ -75,9 +77,19 @@ function PanelHeader({
</Tag>
) : null;

if (isInline && !status) {
return null;
}

return (
<Flex gap="md" align="center" borderBottom="primary" padding="xl">
<Heading as="h3">{displayName}</Heading>
<Flex
gap="md"
align="center"
borderBottom="primary"
padding="xl"
height={USAGE_OVERVIEW_PANEL_HEADER_HEIGHT}
>
{!isInline && <Heading as="h3">{displayName}</Heading>}
{status}
</Flex>
);
Expand Down Expand Up @@ -141,7 +153,7 @@ function ProductBreakdownPanel({
);
}

const isEmpty = !potentialProductTrial && !isEnabled;
const shouldShowUpgradeCta = !potentialProductTrial && !isEnabled;

return (
<Container
Expand All @@ -157,7 +169,11 @@ function ProductBreakdownPanel({
: undefined
}
>
<PanelHeader selectedProduct={selectedProduct} subscription={subscription} />
<PanelHeader
selectedProduct={selectedProduct}
subscription={subscription}
isInline={isInline}
/>
{potentialProductTrial && (
<ProductTrialCta
organization={organization}
Expand All @@ -178,7 +194,9 @@ function ProductBreakdownPanel({
/>
</Fragment>
)}
{isEmpty && <UpgradeCta organization={organization} subscription={subscription} />}
{shouldShowUpgradeCta && (
<UpgradeCta organization={organization} subscription={subscription} />
)}
</Container>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary';

import {DataCategory} from 'sentry/types/core';

import {GIGABYTE, UNLIMITED, UNLIMITED_RESERVED} from 'getsentry/constants';
import {GIGABYTE, UNLIMITED_RESERVED} from 'getsentry/constants';
import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import UsageOverviewTable from 'getsentry/views/subscriptionPage/usageOverview/components/table';

Expand Down Expand Up @@ -163,7 +163,7 @@ describe('UsageOverviewTable', () => {
await screen.findByRole('columnheader', {name: 'Feature'});

// Errors usage and gifted units
expect(screen.getByRole('cell', {name: '6K / 51K'})).toBeInTheDocument();
expect(screen.getByRole('cell', {name: '6K / 51K (1K gifted)'})).toBeInTheDocument();
expect(screen.getByRole('cell', {name: '$10.00'})).toBeInTheDocument();

// Attachments usage should be in the correct unit + above platform volume
Expand All @@ -175,7 +175,7 @@ describe('UsageOverviewTable', () => {
expect(screen.getByRole('cell', {name: '$32.00'})).toBeInTheDocument();

// Unlimited usage for Replays
expect(screen.getByRole('cell', {name: `500K / ${UNLIMITED}`})).toBeInTheDocument();
expect(screen.getByRole('cell', {name: 'Unlimited'})).toBeInTheDocument();
});

it('renders table based on add-on state', async () => {
Expand Down Expand Up @@ -226,7 +226,7 @@ describe('UsageOverviewTable', () => {

// issue fixes is unlimited
expect(screen.getByRole('cell', {name: 'Issue Fixes'})).toBeInTheDocument();
expect(screen.getByRole('cell', {name: `0 / ${UNLIMITED}`})).toBeInTheDocument();
expect(screen.getByRole('cell', {name: 'Unlimited'})).toBeInTheDocument();

// issue scans is 0 so is not rendered
expect(screen.queryByRole('cell', {name: 'Issue Scans'})).not.toBeInTheDocument();
Expand Down
Loading
Loading