Skip to content

feat(seer): revamped checkout #91815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 19, 2025
4 changes: 4 additions & 0 deletions static/gsApp/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export type ReservedBudgetCategory = {
* The API name of the budget
*/
apiName: ReservedBudgetCategoryType;
/**
* The feature flag determining if the product is available for billing
*/
billingFlag: string | null;
/**
* Backend name of the category (all caps, snake case)
*/
Expand Down
52 changes: 16 additions & 36 deletions static/gsApp/views/amCheckout/checkoutOverview.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import {OnDemandBudgetMode, PlanTier} from 'getsentry/types';
import AMCheckout from 'getsentry/views/amCheckout/';
import CheckoutOverview from 'getsentry/views/amCheckout/checkoutOverview';
import type {CheckoutFormData} from 'getsentry/views/amCheckout/types';
import {type CheckoutFormData, SelectableProduct} from 'getsentry/views/amCheckout/types';

describe('CheckoutOverview', function () {
const api = new MockApiClient();
Expand Down Expand Up @@ -175,12 +175,16 @@ describe('CheckoutOverview', function () {
expect(screen.queryByTestId('on-demand-additional-cost')).not.toBeInTheDocument();
});

it('displays Seer Agent AI when enabled and feature flag is present', () => {
it('displays Seer Agent AI when enabled', () => {
const orgWithSeerFeature = {...organization, features: ['seer-billing']};
const formData: CheckoutFormData = {
plan: 'am2_team',
reserved: {errors: 100000, transactions: 500000, attachments: 25},
seerEnabled: true,
selectedProducts: {
[SelectableProduct.SEER]: {
enabled: true,
},
},
};

render(
Expand All @@ -194,19 +198,20 @@ describe('CheckoutOverview', function () {
/>
);

expect(screen.getByTestId('seer')).toBeInTheDocument();
expect(screen.getByText('Seer: Sentry AI Enhancements')).toBeInTheDocument();
expect(
screen.getByText('Surface insights and propose solutions to fix bugs faster.')
).toBeInTheDocument();
expect(screen.getByTestId('seer-reserved')).toBeInTheDocument();
expect(screen.getByText('Seer')).toBeInTheDocument();
});

it('does not display Seer Agent AI when seerEnabled is false', () => {
it('does not display Seer Agent AI when not bought', () => {
const orgWithSeerFeature = {...organization, features: ['seer-billing']};
const formData: CheckoutFormData = {
plan: 'am2_team',
reserved: {errors: 100000, transactions: 500000, attachments: 25},
seerEnabled: false,
selectedProducts: {
[SelectableProduct.SEER]: {
enabled: false,
},
},
};

render(
Expand All @@ -221,31 +226,6 @@ describe('CheckoutOverview', function () {
);

expect(screen.queryByTestId('seer')).not.toBeInTheDocument();
expect(screen.queryByText('Seer: Sentry AI Enhancements')).not.toBeInTheDocument();
});

it('does not display Seer Agent AI when enabled but feature flag is missing', () => {
const orgWithoutSeerFeature = {...organization, features: []};
const formData: CheckoutFormData = {
plan: 'am2_team',
reserved: {errors: 100000, transactions: 500000, attachments: 25},
seerEnabled: true,
};

jest.spyOn(CheckoutOverview.prototype, 'renderSeer').mockReturnValue(null);

render(
<CheckoutOverview
activePlan={teamPlanMonthly}
billingConfig={billingConfig}
formData={formData}
onUpdate={jest.fn()}
organization={orgWithoutSeerFeature}
subscription={subscription}
/>
);

expect(screen.queryByTestId('seer')).not.toBeInTheDocument();
expect(screen.queryByText('Seer: Sentry AI Enhancements')).not.toBeInTheDocument();
expect(screen.queryByText('Seer')).not.toBeInTheDocument();
});
});
85 changes: 40 additions & 45 deletions static/gsApp/views/amCheckout/checkoutOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import PanelBody from 'sentry/components/panels/panelBody';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types/organization';

import {ANNUAL, MONTHLY, SEER_MONTHLY_PRICE_CENTS} from 'getsentry/constants';
import type {BillingConfig, Plan, Promotion, Subscription} from 'getsentry/types';
import {toTitleCase} from 'sentry/utils/string/toTitleCase';

import {ANNUAL, MONTHLY} from 'getsentry/constants';
import type {
BillingConfig,
Plan,
Promotion,
ReservedBudgetCategoryType,
Subscription,
} from 'getsentry/types';
import {OnDemandBudgetMode} from 'getsentry/types';
import {formatReservedWithUnits} from 'getsentry/utils/billing';
import {getPlanCategoryName} from 'getsentry/utils/dataCategory';
Expand Down Expand Up @@ -64,6 +71,35 @@ class CheckoutOverview extends Component<Props> {
}
};

renderProducts = () => {
const {formData, activePlan} = this.props;

return Object.entries(formData.selectedProducts ?? {}).map(([apiName, product]) => {
const productInfo =
activePlan.availableReservedBudgetTypes[apiName as ReservedBudgetCategoryType];
if (!productInfo || !product.enabled) {
return null;
}
const price = utils.displayPrice({
cents: utils.getReservedPriceForReservedBudgetCategory({
plan: activePlan,
reservedBudgetCategory: productInfo.apiName,
}),
});
return (
<DetailItem
key={productInfo.apiName}
data-test-id={`${productInfo.apiName}-reserved`}
>
<DetailTitle>{toTitleCase(productInfo.productName)}</DetailTitle>
<DetailPrice>
{price}/{this.shortInterval}
</DetailPrice>
</DetailItem>
);
});
};

renderDataOptions = () => {
const {formData, activePlan} = this.props;

Expand Down Expand Up @@ -174,47 +210,6 @@ class CheckoutOverview extends Component<Props> {
);
}

renderAdditionalFeature({
featureKey,
title,
description,
priceCents,
enabledField,
}: {
description: string;
enabledField: string;
featureKey: string;
priceCents: number;
title: string;
}) {
const {formData} = this.props;
const isEnabled = formData[enabledField as keyof CheckoutFormData];

if (!isEnabled) {
return null;
}

return (
<DetailItem key={featureKey} data-test-id={featureKey}>
<div>
<DetailTitle>{title}</DetailTitle>
{description}
</div>
<DetailPrice>{`${utils.displayPrice({cents: priceCents})}/mo`}</DetailPrice>
</DetailItem>
);
}

renderSeer() {
return this.renderAdditionalFeature({
featureKey: 'seer',
title: t('Seer: Sentry AI Enhancements'),
description: t('Surface insights and propose solutions to fix bugs faster.'),
priceCents: SEER_MONTHLY_PRICE_CENTS,
enabledField: 'seerEnabled',
});
}

renderDetailItems = () => {
const {activePlan, discountInfo} = this.props;

Expand Down Expand Up @@ -264,8 +259,8 @@ class CheckoutOverview extends Component<Props> {
)}
</PriceContainer>
</DetailItem>
{this.renderProducts()}
{this.renderDataOptions()}
{this.renderSeer()}
{this.renderOnDemand()}
</Fragment>
);
Expand Down
74 changes: 72 additions & 2 deletions static/gsApp/views/amCheckout/checkoutOverviewV2.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import {PlanTier} from 'getsentry/types';
import AMCheckout from 'getsentry/views/amCheckout/';
import CheckoutOverviewV2 from 'getsentry/views/amCheckout/checkoutOverviewV2';
import type {CheckoutFormData} from 'getsentry/views/amCheckout/types';
import {type CheckoutFormData, SelectableProduct} from 'getsentry/views/amCheckout/types';

describe('CheckoutOverviewV2', function () {
const api = new MockApiClient();
Expand Down Expand Up @@ -81,6 +81,11 @@ describe('CheckoutOverviewV2', function () {
uptime: 1,
},
onDemandMaxSpend: 5000,
selectedProducts: {
[SelectableProduct.SEER]: {
enabled: true,
},
},
};

render(
Expand All @@ -94,8 +99,8 @@ describe('CheckoutOverviewV2', function () {
/>
);

expect(screen.getByText('Sentry Team Plan')).toBeInTheDocument();
expect(screen.getByText('All Sentry Products')).toBeInTheDocument();
expect(screen.getByTestId('seer-reserved')).toHaveTextContent('Seer$216/yr');
expect(screen.getByText('Total Annual Charges')).toBeInTheDocument();
expect(screen.getByText('$312/yr')).toBeInTheDocument();
expect(screen.getByTestId('additional-monthly-charge')).toHaveTextContent(
Expand Down Expand Up @@ -161,4 +166,69 @@ describe('CheckoutOverviewV2', function () {
expect(screen.queryByTestId('additional-monthly-charge')).not.toBeInTheDocument();
expect(screen.getAllByText('Product not available')[0]).toBeInTheDocument();
});

it('does not show seer when not enabled', function () {
const formData: CheckoutFormData = {
plan: 'am3_team_auf',
reserved: {
errors: 100000,
attachments: 25,
replays: 50,
spans: 10_000_000,
monitorSeats: 1,
profileDuration: 0,
profileDurationUI: 0,
uptime: 1,
},
onDemandMaxSpend: 5000,
selectedProducts: {
[SelectableProduct.SEER]: {
enabled: false,
},
},
};

render(
<CheckoutOverviewV2
activePlan={teamPlanAnnual}
billingConfig={billingConfig}
formData={formData}
onUpdate={jest.fn()}
organization={organization}
subscription={subscription}
/>
);

expect(screen.queryByTestId('seer-reserved')).not.toBeInTheDocument();
});

it('does not show seer when not included in formData', function () {
const formData: CheckoutFormData = {
plan: 'am3_team_auf',
reserved: {
errors: 100000,
attachments: 25,
replays: 50,
spans: 10_000_000,
monitorSeats: 1,
profileDuration: 0,
profileDurationUI: 0,
uptime: 1,
},
onDemandMaxSpend: 5000,
};

render(
<CheckoutOverviewV2
activePlan={teamPlanAnnual}
billingConfig={billingConfig}
formData={formData}
onUpdate={jest.fn()}
organization={organization}
subscription={subscription}
/>
);

expect(screen.queryByTestId('seer-reserved')).not.toBeInTheDocument();
});
});
Loading
Loading