diff --git a/static/app/constants/index.tsx b/static/app/constants/index.tsx index 1a9afad6e881eb..04287987748ed4 100644 --- a/static/app/constants/index.tsx +++ b/static/app/constants/index.tsx @@ -607,6 +607,20 @@ export const DATA_CATEGORY_INFO = { showExternalStats: true, }, }, + [DataCategoryExact.SEER_USER]: { + name: DataCategoryExact.SEER_USER, + plural: DataCategory.SEER_USER, + singular: 'seerUser', + displayName: 'seer user', + titleName: t('Seer'), + productName: t('Seer'), + uid: 34, + isBilledCategory: true, + statsInfo: { + ...DEFAULT_STATS_INFO, + showExternalStats: false, // TODO(seer): add external stats when ready + }, + }, } as const satisfies Record; // SmartSearchBar settings diff --git a/static/app/types/core.tsx b/static/app/types/core.tsx index 3465bb84c8d79f..e0ee2d9c2715d2 100644 --- a/static/app/types/core.tsx +++ b/static/app/types/core.tsx @@ -93,6 +93,7 @@ export enum DataCategory { LOG_BYTE = 'logBytes', SEER_AUTOFIX = 'seerAutofix', SEER_SCANNER = 'seerScanner', + SEER_USER = 'seerUsers', PREVENT_USER = 'preventUsers', PREVENT_REVIEW = 'preventReviews', USER_REPORT_V2 = 'feedback', @@ -126,6 +127,7 @@ export enum DataCategoryExact { LOG_BYTE = 'log_byte', SEER_AUTOFIX = 'seer_autofix', SEER_SCANNER = 'seer_scanner', + SEER_USER = 'seer_user', PREVENT_USER = 'prevent_user', PREVENT_REVIEW = 'prevent_review', USER_REPORT_V2 = 'feedback', diff --git a/static/gsAdmin/components/changePlanAction.spec.tsx b/static/gsAdmin/components/changePlanAction.spec.tsx index 1c06f52dfea5c8..4a09faea09deb7 100644 --- a/static/gsAdmin/components/changePlanAction.spec.tsx +++ b/static/gsAdmin/components/changePlanAction.spec.tsx @@ -6,7 +6,7 @@ import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory'; import {PlanDetailsLookupFixture} from 'getsentry-test/fixtures/planDetailsLookup'; import { SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import { renderGlobalModal, @@ -268,8 +268,10 @@ describe('ChangePlanAction', () => { await selectEvent.select(screen.getByRole('textbox', {name: 'Logs (GB)'}), '5'); expect(screen.getByText('Available Products')).toBeInTheDocument(); - await userEvent.click(screen.getByText('Seer')); - await userEvent.click(screen.getByText('Prevent')); + const seerSelections = screen.getAllByText('Seer'); + expect(seerSelections).toHaveLength(2); + await userEvent.click(seerSelections[0]!); + await userEvent.click(seerSelections[1]!); expect(screen.getByRole('button', {name: 'Change Plan'})).toBeEnabled(); await userEvent.click(screen.getByRole('button', {name: 'Change Plan'})); @@ -277,8 +279,8 @@ describe('ChangePlanAction', () => { expect(putMock).toHaveBeenCalled(); const requestData = putMock.mock.calls[0][1].data; expect(requestData).toHaveProperty('plan', 'am3_business'); + expect(requestData).toHaveProperty('addOnLegacySeer', true); expect(requestData).toHaveProperty('addOnSeer', true); - expect(requestData).toHaveProperty('addOnPrevent', true); }); it('updates plan list when switching between tiers', async () => { @@ -447,7 +449,7 @@ describe('ChangePlanAction', () => { it('initializes Seer budget checkbox based on current subscription', async () => { // Create subscription with Seer budget - const subscriptionWithSeer = SubscriptionWithSeerFixture({ + const subscriptionWithSeer = SubscriptionWithLegacySeerFixture({ organization: mockOrg, planTier: PlanTier.AM3, plan: 'am3_business', @@ -543,7 +545,7 @@ describe('ChangePlanAction', () => { // Verify the PUT API was called with seer parameter expect(putMock).toHaveBeenCalled(); const requestData = putMock.mock.calls[0][1].data; - expect(requestData).toHaveProperty('addOnSeer', true); + expect(requestData).toHaveProperty('addOnLegacySeer', true); }); it('does not include add-on parameter in form submission when checkbox is unchecked', async () => { @@ -595,7 +597,7 @@ describe('ChangePlanAction', () => { // Verify the PUT API was called with seer parameter set to false expect(putMock).toHaveBeenCalled(); const requestData = putMock.mock.calls[0][1].data; - expect(requestData).toHaveProperty('addOnSeer', false); + expect(requestData).toHaveProperty('addOnLegacySeer', false); }); }); }); diff --git a/static/gsAdmin/components/customers/customerOverview.spec.tsx b/static/gsAdmin/components/customers/customerOverview.spec.tsx index 71ab58156d6796..d2eb4b72c6cb0e 100644 --- a/static/gsAdmin/components/customers/customerOverview.spec.tsx +++ b/static/gsAdmin/components/customers/customerOverview.spec.tsx @@ -6,7 +6,7 @@ import {SeerReservedBudgetFixture} from 'getsentry-test/fixtures/reservedBudget' import { InvoicedSubscriptionFixture, SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import { render, @@ -307,7 +307,7 @@ describe('CustomerOverview', () => { it('renders reserved budget data', () => { const organization = OrganizationFixture(); - const subscription = SubscriptionWithSeerFixture({organization}); + const subscription = SubscriptionWithLegacySeerFixture({organization}); subscription.reservedBudgets = [ SeerReservedBudgetFixture({ totalReservedSpend: 20_00, diff --git a/static/gsApp/constants.tsx b/static/gsApp/constants.tsx index 0f9d5687931b67..8cef22dc316588 100644 --- a/static/gsApp/constants.tsx +++ b/static/gsApp/constants.tsx @@ -207,11 +207,11 @@ export const BILLED_DATA_CATEGORY_INFO = { ), shortenedUnitName: 'GB', }, - [DataCategoryExact.PREVENT_USER]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.PREVENT_USER], + [DataCategoryExact.SEER_USER]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_USER], feature: 'seer-user-billing', canProductTrial: true, - maxAdminGift: 10_000, // TODO(prevent): Update this to the actual max admin gift + maxAdminGift: 10_000, // TODO(seer): Update this to the actual max admin gift tallyType: 'seat', }, } as const satisfies Record; diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index 6d6bae5261fd44..6e58d8941f5ec5 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -137,7 +137,6 @@ export type ReservedBudgetCategory = { export enum AddOnCategory { SEER = 'seer', - PREVENT = 'prevent', LEGACY_SEER = 'legacySeer', } diff --git a/static/gsApp/utils/billing.tsx b/static/gsApp/utils/billing.tsx index 2df749265d4c28..48da32b511e1f8 100644 --- a/static/gsApp/utils/billing.tsx +++ b/static/gsApp/utils/billing.tsx @@ -1,7 +1,7 @@ import moment from 'moment-timezone'; import type {PromptData} from 'sentry/actionCreators/prompts'; -import {IconBuilding, IconGroup, IconPrevent, IconSeer, IconUser} from 'sentry/icons'; +import {IconBuilding, IconGroup, IconSeer, IconUser} from 'sentry/icons'; import {DataCategory} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; @@ -589,14 +589,10 @@ export function getPlanIcon(plan: Plan) { } export function getProductIcon(product: AddOnCategory, size?: IconSize) { - switch (product) { - case AddOnCategory.SEER: - return ; - case AddOnCategory.PREVENT: - return ; - default: - return null; + if ([AddOnCategory.LEGACY_SEER, AddOnCategory.SEER].includes(product)) { + return ; } + return null; } /** @@ -783,12 +779,10 @@ export function hasSomeBillingDetails(billingDetails: BillingDetails | undefined } export function getReservedBudgetCategoryForAddOn(addOnCategory: AddOnCategory) { - switch (addOnCategory) { - case AddOnCategory.SEER: - return ReservedBudgetCategoryType.SEER; - default: - return null; + if (addOnCategory === AddOnCategory.LEGACY_SEER) { + return ReservedBudgetCategoryType.SEER; } + return null; } // There are the data categories whose retention settings diff --git a/static/gsApp/views/amCheckout/components/cart.spec.tsx b/static/gsApp/views/amCheckout/components/cart.spec.tsx index bf1da81fe12cc4..b7272d02c691fd 100644 --- a/static/gsApp/views/amCheckout/components/cart.spec.tsx +++ b/static/gsApp/views/amCheckout/components/cart.spec.tsx @@ -138,7 +138,7 @@ describe('Cart', () => { }, onDemandMaxSpend: 50_00, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: true, }, }, @@ -165,7 +165,7 @@ describe('Cart', () => { expect(planItem).toHaveTextContent('Continuous profile hours'); expect(planItem).toHaveTextContent('Available'); - const seerItem = screen.getByTestId('summary-item-product-seer'); + const seerItem = screen.getByTestId('summary-item-product-legacySeer'); expect(seerItem).toHaveTextContent('Seer'); expect(seerItem).toHaveTextContent('$216/yr'); @@ -496,7 +496,7 @@ describe('Cart', () => { sharedMaxBudget: 1_00, }, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: true, }, }, diff --git a/static/gsApp/views/amCheckout/components/cartDiff.spec.tsx b/static/gsApp/views/amCheckout/components/cartDiff.spec.tsx index d22a5e8e5d5949..382514b6b5c2dc 100644 --- a/static/gsApp/views/amCheckout/components/cartDiff.spec.tsx +++ b/static/gsApp/views/amCheckout/components/cartDiff.spec.tsx @@ -67,7 +67,7 @@ describe('CartDiff', () => { sharedMaxBudget: 100_00, }, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: true, }, }, diff --git a/static/gsApp/views/amCheckout/components/checkoutOverview.spec.tsx b/static/gsApp/views/amCheckout/components/checkoutOverview.spec.tsx index ea4bd7352f5f20..e33ef745eda2dc 100644 --- a/static/gsApp/views/amCheckout/components/checkoutOverview.spec.tsx +++ b/static/gsApp/views/amCheckout/components/checkoutOverview.spec.tsx @@ -180,7 +180,7 @@ describe('CheckoutOverview', () => { plan: 'am2_team', reserved: {errors: 100000, transactions: 500000, attachments: 25}, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: true, }, }, @@ -197,7 +197,7 @@ describe('CheckoutOverview', () => { /> ); - expect(screen.getByTestId('seer-reserved')).toBeInTheDocument(); + expect(screen.getByTestId('legacySeer-reserved')).toBeInTheDocument(); expect(screen.getByText('Seer')).toBeInTheDocument(); }); @@ -207,7 +207,7 @@ describe('CheckoutOverview', () => { plan: 'am2_team', reserved: {errors: 100000, transactions: 500000, attachments: 25}, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: false, }, }, @@ -224,7 +224,7 @@ describe('CheckoutOverview', () => { /> ); - expect(screen.queryByTestId('seer')).not.toBeInTheDocument(); + expect(screen.queryByTestId('legacySeer-reserved')).not.toBeInTheDocument(); expect(screen.queryByText('Seer')).not.toBeInTheDocument(); }); }); diff --git a/static/gsApp/views/amCheckout/components/checkoutOverviewV2.spec.tsx b/static/gsApp/views/amCheckout/components/checkoutOverviewV2.spec.tsx index 07477c58e0607e..ccbbe7b0fd0612 100644 --- a/static/gsApp/views/amCheckout/components/checkoutOverviewV2.spec.tsx +++ b/static/gsApp/views/amCheckout/components/checkoutOverviewV2.spec.tsx @@ -80,7 +80,7 @@ describe('CheckoutOverviewV2', () => { }, onDemandMaxSpend: 5000, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: true, }, }, @@ -97,7 +97,7 @@ describe('CheckoutOverviewV2', () => { /> ); - expect(screen.getByTestId('seer-reserved')).toHaveTextContent('Seer$216/yr'); + expect(screen.getByTestId('legacySeer-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( @@ -179,7 +179,7 @@ describe('CheckoutOverviewV2', () => { }, onDemandMaxSpend: 5000, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: false, }, }, @@ -196,7 +196,7 @@ describe('CheckoutOverviewV2', () => { /> ); - expect(screen.queryByTestId('seer-reserved')).not.toBeInTheDocument(); + expect(screen.queryByTestId('legacySeer-reserved')).not.toBeInTheDocument(); }); it('does not show add-on when not included in formData', () => { @@ -226,6 +226,6 @@ describe('CheckoutOverviewV2', () => { /> ); - expect(screen.queryByTestId('seer-reserved')).not.toBeInTheDocument(); + expect(screen.queryByTestId('legacySeer-reserved')).not.toBeInTheDocument(); }); }); diff --git a/static/gsApp/views/amCheckout/components/checkoutSuccess.tsx b/static/gsApp/views/amCheckout/components/checkoutSuccess.tsx index d3038618ce1744..e07be5b5d983a5 100644 --- a/static/gsApp/views/amCheckout/components/checkoutSuccess.tsx +++ b/static/gsApp/views/amCheckout/components/checkoutSuccess.tsx @@ -191,7 +191,7 @@ function ScheduledChanges({ )} {products.map(item => { - const addOn = utils.invoiceItemTypeToAddOn(item.type); + const addOn = utils.reservedInvoiceItemTypeToAddOn(item.type); if (!addOn) { return null; } @@ -515,7 +515,7 @@ function CheckoutSuccess({ const reservedVolume = invoiceItems.filter( item => item.type.startsWith('reserved_') && !item.type.endsWith('_budget') ); - // TODO(prevent): This needs to be updated once we determine how to display Prevent enablement and PAYG changes on this page + // TODO(seer): This needs to be updated once we determine how to display Seer enablement and PAYG changes on this page const products = invoiceItems.filter(item => item.type === 'reserved_seer_budget'); const onDemandItems = getOnDemandItems({invoiceItems}); const fees = getFees({invoiceItems}); diff --git a/static/gsApp/views/amCheckout/index.spec.tsx b/static/gsApp/views/amCheckout/index.spec.tsx index ce26389991c057..f838e7eda9aec8 100644 --- a/static/gsApp/views/amCheckout/index.spec.tsx +++ b/static/gsApp/views/amCheckout/index.spec.tsx @@ -6,7 +6,7 @@ import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig'; import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory'; import { SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import { act, @@ -1119,7 +1119,7 @@ describe('AM2 Checkout', () => { it('skips step 1 for business plan with seer', async () => { const seerOrg = OrganizationFixture({features: ['seer-billing']}); - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization: seerOrg, planTier: 'am2', plan: 'am2_business', diff --git a/static/gsApp/views/amCheckout/steps/productSelect.spec.tsx b/static/gsApp/views/amCheckout/steps/productSelect.spec.tsx index 4a24c43eaa1f82..e5f9fafaa89572 100644 --- a/static/gsApp/views/amCheckout/steps/productSelect.spec.tsx +++ b/static/gsApp/views/amCheckout/steps/productSelect.spec.tsx @@ -5,7 +5,7 @@ import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig'; import {SeerReservedBudgetFixture} from 'getsentry-test/fixtures/reservedBudget'; import { SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; @@ -80,7 +80,7 @@ describe('ProductSelect', () => { {organization} ); - expect(await screen.findByTestId('product-option-seer')).toBeInTheDocument(); + expect(await screen.findByTestId('product-option-legacySeer')).toBeInTheDocument(); expect(screen.getAllByTestId(/product-option-feature/)).toHaveLength(2); expect(screen.getAllByTestId(/product-option/)).toHaveLength(3); expect(screen.getByText('Add to plan')).toBeInTheDocument(); @@ -107,7 +107,7 @@ describe('ProductSelect', () => { {organization} ); - expect(await screen.findByTestId('product-option-seer')).toBeInTheDocument(); + expect(await screen.findByTestId('product-option-legacySeer')).toBeInTheDocument(); expect(screen.getAllByTestId(/product-option-feature/)).toHaveLength(3); // +1 for credits included expect(screen.getAllByTestId(/product-option/)).toHaveLength(4); // +1 for credits included expect(screen.queryByText('Add to plan')).not.toBeInTheDocument(); @@ -152,7 +152,7 @@ describe('ProductSelect', () => { {organization} ); - const seerProduct = await screen.findByTestId('product-option-seer'); + const seerProduct = await screen.findByTestId('product-option-legacySeer'); expect(seerProduct).toHaveTextContent('$20/mo'); expect(seerProduct).toHaveTextContent('$25/mo in credits towards'); }); @@ -175,13 +175,13 @@ describe('ProductSelect', () => { {organization} ); - const seerProduct = await screen.findByTestId('product-option-seer'); + const seerProduct = await screen.findByTestId('product-option-legacySeer'); expect(seerProduct).toHaveTextContent('$216/yr'); expect(seerProduct).toHaveTextContent('$25/mo in credits towards'); }); it('renders with product selected based on current subscription', async () => { - const seerSubscription = SubscriptionWithSeerFixture({organization}); + const seerSubscription = SubscriptionWithLegacySeerFixture({organization}); SubscriptionStore.set(organization.slug, seerSubscription); render( @@ -195,7 +195,7 @@ describe('ProductSelect', () => { {organization} ); - expect(await screen.findByTestId('product-option-seer')).toHaveTextContent( + expect(await screen.findByTestId('product-option-legacySeer')).toHaveTextContent( 'Added to plan' ); }); @@ -216,7 +216,7 @@ describe('ProductSelect', () => { {organization} ); - expect(await screen.findByTestId('product-option-seer')).toHaveTextContent( + expect(await screen.findByTestId('product-option-legacySeer')).toHaveTextContent( 'Add to plan' ); }); @@ -233,7 +233,7 @@ describe('ProductSelect', () => { {organization} ); - const seerProduct = await screen.findByTestId('product-option-seer'); + const seerProduct = await screen.findByTestId('product-option-legacySeer'); const seerButton = within(seerProduct).getByRole('button'); expect(seerButton).toHaveTextContent('Add to plan'); await userEvent.click(seerButton); diff --git a/static/gsApp/views/amCheckout/steps/productSelect.tsx b/static/gsApp/views/amCheckout/steps/productSelect.tsx index a4b4293f67f23e..8c5f5d2286cf5a 100644 --- a/static/gsApp/views/amCheckout/steps/productSelect.tsx +++ b/static/gsApp/views/amCheckout/steps/productSelect.tsx @@ -1,4 +1,4 @@ -import React, {Fragment} from 'react'; +import {Fragment} from 'react'; import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; @@ -45,7 +45,6 @@ interface ProductCheckoutInfo { } > >; - getProductDescription: (includedBudget: string) => React.ReactNode; buttonBorderColor?: Color; color?: Color; gradientEndColor?: Color; @@ -62,7 +61,7 @@ export function getProductCheckoutDescription({ withPunctuation: boolean; includedBudget?: string; }) { - if (product === AddOnCategory.SEER) { + if (product === AddOnCategory.LEGACY_SEER) { if (isNewCheckout) { return tct('Detect and fix issues faster with our AI agent[punctuation]', { punctuation: withPunctuation ? '.' : '', @@ -80,6 +79,18 @@ export function getProductCheckoutDescription({ } ); } + + if (product === AddOnCategory.SEER) { + return ( + + + {t('Setup required: connect repositories after adding to your plan.')} + + {t('Billed at month-end and varies with active contributors.')} + + ); + } + return ''; } @@ -96,52 +107,32 @@ function ProductSelect({ ); const theme = useTheme(); - const SEER_CHECKOUT_INFO = { - color: theme.pink400 as Color, - gradientEndColor: theme.pink100 as Color, - buttonBorderColor: theme.pink200 as Color, - getProductDescription: (includedBudget: string) => - getProductCheckoutDescription({ - product: AddOnCategory.SEER, - isNewCheckout: !!isNewCheckout, - withPunctuation: false, - includedBudget, - }), - categoryInfo: { - [DataCategory.SEER_AUTOFIX]: { - description: t( - 'Uses the latest AI models with Sentry data to find root causes & proposes PRs' - ), - maxEventPriceDigits: 0, - }, - [DataCategory.SEER_SCANNER]: { - description: t( - 'Triages issues as they happen, automatically flagging highly-fixable ones for followup' - ), - maxEventPriceDigits: 3, - }, - }, - }; const PRODUCT_CHECKOUT_INFO = { - [AddOnCategory.SEER]: SEER_CHECKOUT_INFO, - [AddOnCategory.LEGACY_SEER]: SEER_CHECKOUT_INFO, - [AddOnCategory.PREVENT]: { - getProductDescription: (includedBudget: string) => - getProductCheckoutDescription({ - product: AddOnCategory.PREVENT, - isNewCheckout: !!isNewCheckout, - withPunctuation: false, - includedBudget, - }), + [AddOnCategory.LEGACY_SEER]: { + color: theme.pink400 as Color, + gradientEndColor: theme.pink100 as Color, + buttonBorderColor: theme.pink200 as Color, categoryInfo: { - [DataCategory.PREVENT_USER]: { - description: t('Prevent problems, before they happen'), + [DataCategory.SEER_AUTOFIX]: { + description: t( + 'Uses the latest AI models with Sentry data to find root causes & proposes PRs' + ), maxEventPriceDigits: 0, }, + [DataCategory.SEER_SCANNER]: { + description: t( + 'Triages issues as they happen, automatically flagging highly-fixable ones for followup' + ), + maxEventPriceDigits: 3, + }, }, - color: undefined, + }, + [AddOnCategory.SEER]: { + categoryInfo: {}, + // TODO(isabella): These can be removed once the legacy implementation for this component is removed gradientEndColor: undefined, buttonBorderColor: undefined, + color: undefined, }, } satisfies Record; const billingInterval = utils.getShortInterval(activePlan.billingInterval); @@ -321,7 +312,12 @@ function ProductSelect({ - {checkoutInfo.getProductDescription(formattedMonthlyBudget ?? '')} + {getProductCheckoutDescription({ + product: apiName, + isNewCheckout: !!isNewCheckout, + withPunctuation: false, + includedBudget: formattedMonthlyBudget ?? '', + })} diff --git a/static/gsApp/views/amCheckout/steps/reviewAndConfirm.spec.tsx b/static/gsApp/views/amCheckout/steps/reviewAndConfirm.spec.tsx index 62875c701a82a9..861936b1e3df6e 100644 --- a/static/gsApp/views/amCheckout/steps/reviewAndConfirm.spec.tsx +++ b/static/gsApp/views/amCheckout/steps/reviewAndConfirm.spec.tsx @@ -243,7 +243,7 @@ describe('AmCheckout > ReviewAndConfirm', () => { ...formData, reserved: {...formData.reserved, errors: reservedErrors}, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: true, }, }, @@ -286,7 +286,7 @@ describe('AmCheckout > ReviewAndConfirm', () => { expect(trackGetsentryAnalytics).toHaveBeenCalledWith('checkout.product_select', { organization, subscription, - seer: { + legacySeer: { enabled: true, previously_enabled: false, }, diff --git a/static/gsApp/views/amCheckout/utils.spec.tsx b/static/gsApp/views/amCheckout/utils.spec.tsx index 99ce834b7f2634..dfde0a45e568db 100644 --- a/static/gsApp/views/amCheckout/utils.spec.tsx +++ b/static/gsApp/views/amCheckout/utils.spec.tsx @@ -12,7 +12,7 @@ describe('utils', () => { const am3TeamPlan = PlanDetailsLookupFixture('am3_team')!; const am3TeamPlanAnnual = PlanDetailsLookupFixture('am3_team_auf')!; const DEFAULT_ADDONS = { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: false, }, }; @@ -93,7 +93,7 @@ describe('utils', () => { attachments: 1, }, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: true, }, }, @@ -111,7 +111,7 @@ describe('utils', () => { attachments: 1, }, addOns: { - [AddOnCategory.SEER]: { + [AddOnCategory.LEGACY_SEER]: { enabled: true, }, }, @@ -400,7 +400,7 @@ describe('utils', () => { reservedUptime: 60, reservedAttachments: 70, reservedProfileDuration: 80, - addOnSeer: false, + addOnLegacySeer: false, }); }); }); diff --git a/static/gsApp/views/amCheckout/utils.tsx b/static/gsApp/views/amCheckout/utils.tsx index 25d3196b8a9e0b..601742574f05cc 100644 --- a/static/gsApp/views/amCheckout/utils.tsx +++ b/static/gsApp/views/amCheckout/utils.tsx @@ -635,8 +635,10 @@ export function useSubmitCheckout({ // seer automation alert const alreadyHasSeer = - !isTrialPlan(subscription.plan) && subscription.addOns?.seer?.enabled; - const justBoughtSeer = _variables.data.addOnSeer && !alreadyHasSeer; + !isTrialPlan(subscription.plan) && + (subscription.addOns?.seer?.enabled || subscription.addOns?.legacySeer?.enabled); + const justBoughtSeer = + (_variables.data.addOnLegacySeer || _variables.data.addOnSeer) && !alreadyHasSeer; // refresh org and subscription state // useApi cancels open requests on unmount by default, so we create a new Client to ensure this @@ -742,8 +744,9 @@ export async function submitCheckout( recordAnalytics(organization, subscription, data, isMigratingPartnerAccount); const alreadyHasSeer = - !isTrialPlan(subscription.plan) && subscription.addOns?.seer?.enabled; - const justBoughtSeer = data.addOnSeer && !alreadyHasSeer; + !isTrialPlan(subscription.plan) && + (subscription.addOns?.seer?.enabled || subscription.addOns?.legacySeer?.enabled); + const justBoughtSeer = (data.addOnLegacySeer || data.addOnSeer) && !alreadyHasSeer; // refresh org and subscription state // useApi cancels open requests on unmount by default, so we create a new Client to ensure this @@ -870,12 +873,14 @@ export function invoiceItemTypeToDataCategory( ) as DataCategory; } -export function invoiceItemTypeToAddOn(type: InvoiceItemType): AddOnCategory | null { +export function reservedInvoiceItemTypeToAddOn( + type: InvoiceItemType +): AddOnCategory | null { switch (type) { case 'reserved_seer_budget': + return AddOnCategory.LEGACY_SEER; + case 'reserved_seer_users': return AddOnCategory.SEER; - case 'reserved_prevent_users': - return AddOnCategory.PREVENT; default: return null; } diff --git a/static/gsApp/views/subscriptionPage/headerCards/usageCard.spec.tsx b/static/gsApp/views/subscriptionPage/headerCards/usageCard.spec.tsx index 25471af676223d..d71be45cfeebc9 100644 --- a/static/gsApp/views/subscriptionPage/headerCards/usageCard.spec.tsx +++ b/static/gsApp/views/subscriptionPage/headerCards/usageCard.spec.tsx @@ -3,7 +3,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory'; import { SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import {render, screen} from 'sentry-test/reactTestingLibrary'; @@ -144,7 +144,7 @@ describe('UsageCard', () => { }); it('should not show reserved budget under included in subscription', () => { - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); @@ -158,7 +158,7 @@ describe('UsageCard', () => { }); it('should not calculate prepaid spend with reserved budget', () => { - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); diff --git a/static/gsApp/views/subscriptionPage/overview.spec.tsx b/static/gsApp/views/subscriptionPage/overview.spec.tsx index 9259fda3fdfd39..5e51ae7d4f7639 100644 --- a/static/gsApp/views/subscriptionPage/overview.spec.tsx +++ b/static/gsApp/views/subscriptionPage/overview.spec.tsx @@ -11,7 +11,7 @@ import { Am3DsEnterpriseSubscriptionFixture, InvoicedSubscriptionFixture, SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import { render, @@ -232,7 +232,7 @@ describe('Subscription > Overview', () => { }); it('renders with Seer', async () => { - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization, }); SubscriptionStore.set(organization.slug, seerSubscription); diff --git a/static/gsApp/views/subscriptionPage/reservedUsageChart.spec.tsx b/static/gsApp/views/subscriptionPage/reservedUsageChart.spec.tsx index 3cb617f4e19061..b22d12c892e53c 100644 --- a/static/gsApp/views/subscriptionPage/reservedUsageChart.spec.tsx +++ b/static/gsApp/views/subscriptionPage/reservedUsageChart.spec.tsx @@ -3,7 +3,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import { Am3DsEnterpriseSubscriptionFixture, SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import {act, render, screen} from 'sentry-test/reactTestingLibrary'; @@ -777,7 +777,7 @@ describe('DisplayMode Toggle for Reserved Budget Categories', () => { } it('should respect displayMode="usage" for SEER reserved budget categories', async () => { - const subscription = SubscriptionWithSeerFixture({ + const subscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); @@ -844,7 +844,7 @@ describe('DisplayMode Toggle for Reserved Budget Categories', () => { }); it('should respect displayMode="cost" for SEER reserved budget categories', async () => { - const subscription = SubscriptionWithSeerFixture({ + const subscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); @@ -911,7 +911,7 @@ describe('DisplayMode Toggle for Reserved Budget Categories', () => { }); it('should force displayMode="cost" for sales-led customers with reserved budget categories', async () => { - const subscription = SubscriptionWithSeerFixture({ + const subscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', canSelfServe: false, // Sales-led customer diff --git a/static/gsApp/views/subscriptionPage/usageOverview.spec.tsx b/static/gsApp/views/subscriptionPage/usageOverview.spec.tsx index 8bc5c4bddc75ce..bc97d9759fdefe 100644 --- a/static/gsApp/views/subscriptionPage/usageOverview.spec.tsx +++ b/static/gsApp/views/subscriptionPage/usageOverview.spec.tsx @@ -6,7 +6,7 @@ import {BillingHistoryFixture} from 'getsentry-test/fixtures/billingHistory'; import {CustomerUsageFixture} from 'getsentry-test/fixtures/customerUsage'; import { SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {resetMockDate, setMockDate} from 'sentry-test/utils'; @@ -227,7 +227,7 @@ describe('UsageOverview', () => { it('renders table based on add-on state', () => { organization.features.push('seer-user-billing'); - const subWithSeer = SubscriptionWithSeerFixture({organization}); + const subWithSeer = SubscriptionWithLegacySeerFixture({organization}); SubscriptionStore.set(organization.slug, subWithSeer); render( { usageData={usageData} /> ); - expect(screen.getByRole('cell', {name: 'Seer'})).toBeInTheDocument(); - expect(screen.getByRole('row', {name: 'Collapse Seer details'})).toBeInTheDocument(); + // Org has Seer user flag but did not buy Seer add on, only legacy add-on + expect(screen.getAllByRole('cell', {name: 'Seer'})).toHaveLength(1); + expect(screen.getAllByRole('row', {name: 'Collapse Seer details'})).toHaveLength(1); expect(screen.getByRole('cell', {name: 'Issue Fixes'})).toBeInTheDocument(); expect(screen.getByRole('cell', {name: 'Issue Scans'})).toBeInTheDocument(); - // Org has Prevent flag but did not buy Prevent add on - expect(screen.getByRole('cell', {name: 'Prevent'})).toBeInTheDocument(); - expect( - screen.queryByRole('row', {name: 'Collapse Prevent details'}) - ).not.toBeInTheDocument(); // We test it this way to ensure we don't show the cell with the proper display name or the raw DataCategory - expect(screen.queryByRole('cell', {name: /Prevent*Users/})).not.toBeInTheDocument(); + expect(screen.queryByRole('cell', {name: /Seer*Users/})).not.toBeInTheDocument(); expect(screen.queryByRole('cell', {name: /Prevent*Reviews/})).not.toBeInTheDocument(); }); @@ -322,7 +318,7 @@ describe('UsageOverview', () => { url: `/customers/${organization.slug}/history/`, method: 'GET', }); - const subWithSeer = SubscriptionWithSeerFixture({organization}); + const subWithSeer = SubscriptionWithLegacySeerFixture({organization}); const mockLocation = LocationFixture(); SubscriptionStore.set(organization.slug, subWithSeer); diff --git a/static/gsApp/views/subscriptionPage/usageOverview.tsx b/static/gsApp/views/subscriptionPage/usageOverview.tsx index b30638a65db4bf..b89298e1d4d598 100644 --- a/static/gsApp/views/subscriptionPage/usageOverview.tsx +++ b/static/gsApp/views/subscriptionPage/usageOverview.tsx @@ -236,12 +236,15 @@ function UsageOverviewTable({subscription, organization, usageData}: UsageOvervi // show add-ons regardless of whether they're enabled // as long as they're launched for the org // and none of their sub-categories are unlimited + // Also do not show Seer if the legacy Seer add-on is enabled ([_, addOnInfo]) => (!addOnInfo.billingFlag || organization.features.includes(addOnInfo.billingFlag)) && !addOnInfo.dataCategories.some( category => subscription.categories[category]?.reserved === UNLIMITED_RESERVED - ) + ) && + (addOnInfo.apiName !== AddOnCategory.SEER || + !subscription.addOns?.[AddOnCategory.LEGACY_SEER]?.enabled) ), [subscription.addOns, organization.features, subscription.categories] ); diff --git a/static/gsApp/views/subscriptionPage/usageTotals.spec.tsx b/static/gsApp/views/subscriptionPage/usageTotals.spec.tsx index ec04db59693bff..dbdf047c99fefb 100644 --- a/static/gsApp/views/subscriptionPage/usageTotals.spec.tsx +++ b/static/gsApp/views/subscriptionPage/usageTotals.spec.tsx @@ -10,7 +10,7 @@ import { import { Am3DsEnterpriseSubscriptionFixture, SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import {UsageTotalFixture} from 'getsentry-test/fixtures/usageTotal'; import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; @@ -1057,7 +1057,7 @@ describe('Subscription > CombinedUsageTotals', () => { }); it('always renders reserved budgets in spend mode', async () => { - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', onDemandMaxSpend: 10_00, @@ -1097,7 +1097,7 @@ describe('Subscription > CombinedUsageTotals', () => { }); it('uses billed usage for accepted counts in expanded table', async () => { - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', onDemandMaxSpend: 10_00, @@ -1129,7 +1129,7 @@ describe('Subscription > CombinedUsageTotals', () => { }); it('shows table with dropped totals breakdown for reserved budgets', async () => { - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); @@ -1488,7 +1488,7 @@ describe('Subscription > CombinedUsageTotals', () => { it('renders PAYG legend with per-category', () => { organization.features.push('ondemand-budgets'); - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am2_business', onDemandBudgets: { @@ -1800,7 +1800,7 @@ describe('calculateCategoryPrepaidUsage', () => { }); it('calculates for SEER reserved budgets with automatic extraction', () => { - const subscription = SubscriptionWithSeerFixture({ + const subscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); @@ -1835,7 +1835,7 @@ describe('calculateCategoryPrepaidUsage', () => { }); it('calculates for SEER scanner with different CPE', () => { - const subscription = SubscriptionWithSeerFixture({ + const subscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); @@ -1870,7 +1870,7 @@ describe('calculateCategoryPrepaidUsage', () => { }); it('calculates for SEER reserved budgets when over budget', () => { - const subscription = SubscriptionWithSeerFixture({ + const subscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); @@ -1905,7 +1905,7 @@ describe('calculateCategoryPrepaidUsage', () => { }); it('calculates for SEER reserved budgets with explicit reservedSpend override', () => { - const subscription = SubscriptionWithSeerFixture({ + const subscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); @@ -1938,7 +1938,7 @@ describe('calculateCategoryPrepaidUsage', () => { }); it('calculates for SEER reserved budgets with zero spend', () => { - const subscription = SubscriptionWithSeerFixture({ + const subscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); diff --git a/static/gsApp/views/subscriptionPage/utils.spec.tsx b/static/gsApp/views/subscriptionPage/utils.spec.tsx index b784847cb4c4dc..35eb86a06c08c1 100644 --- a/static/gsApp/views/subscriptionPage/utils.spec.tsx +++ b/static/gsApp/views/subscriptionPage/utils.spec.tsx @@ -2,7 +2,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import { SubscriptionFixture, - SubscriptionWithSeerFixture, + SubscriptionWithLegacySeerFixture, } from 'getsentry-test/fixtures/subscription'; import {DataCategory} from 'sentry/types/core'; @@ -99,7 +99,7 @@ describe('calculateTotalSpend', () => { }); it('should calculate reserved budget spend', () => { - const seerSubscription = SubscriptionWithSeerFixture({ + const seerSubscription = SubscriptionWithLegacySeerFixture({ organization, plan: 'am3_business', }); diff --git a/tests/js/getsentry-test/fixtures/constants.ts b/tests/js/getsentry-test/fixtures/constants.ts index f3e196111f23d1..2c23c370695627 100644 --- a/tests/js/getsentry-test/fixtures/constants.ts +++ b/tests/js/getsentry-test/fixtures/constants.ts @@ -5,20 +5,25 @@ import {AddOnCategory, type AddOnCategoryInfo} from 'getsentry/types'; // TODO(isabella): update this with other common constants in the fixtures export const AM_ADD_ON_CATEGORIES = { - [AddOnCategory.SEER]: { - apiName: AddOnCategory.SEER, + [AddOnCategory.LEGACY_SEER]: { + apiName: AddOnCategory.LEGACY_SEER, dataCategories: [DataCategory.SEER_AUTOFIX, DataCategory.SEER_SCANNER], - name: 'seer', + name: 'legacySeer', billingFlag: 'seer-billing', order: 1, productName: 'Seer', }, - [AddOnCategory.PREVENT]: { - apiName: AddOnCategory.PREVENT, - dataCategories: [DataCategory.PREVENT_USER, DataCategory.PREVENT_REVIEW], - name: 'prevent', + [AddOnCategory.SEER]: { + apiName: AddOnCategory.SEER, + dataCategories: [ + DataCategory.SEER_USER, + DataCategory.PREVENT_REVIEW, + DataCategory.SEER_AUTOFIX, + DataCategory.SEER_SCANNER, + ], + name: 'seer', billingFlag: 'seer-user-billing', order: 2, - productName: 'prevent', + productName: 'Seer', }, -} satisfies Record, AddOnCategoryInfo>; // TODO(seer): Add LEGACY_SEER once the backend is updated to use the new value and the rest of the frontend can be updated +} satisfies Record; diff --git a/tests/js/getsentry-test/fixtures/subscription.ts b/tests/js/getsentry-test/fixtures/subscription.ts index 264ecfe5f04a09..dc3bb92a77af3d 100644 --- a/tests/js/getsentry-test/fixtures/subscription.ts +++ b/tests/js/getsentry-test/fixtures/subscription.ts @@ -38,7 +38,7 @@ export function SubscriptionFixture(props: Props): TSubscription { ); const hasAttachments = planDetails?.categories?.includes(DataCategory.ATTACHMENTS); const hasLogBytes = planDetails?.categories?.includes(DataCategory.LOG_BYTE); - const hasSeer = AddOnCategory.SEER in planDetails.addOnCategories; + const hasLegacySeer = AddOnCategory.LEGACY_SEER in planDetails.addOnCategories; // Create a safe default for planCategories if it doesn't exist const safeCategories = planDetails?.planCategories || {}; @@ -46,7 +46,7 @@ export function SubscriptionFixture(props: Props): TSubscription { const isTrial = isTrialPlan(planDetails.id); const isEnterpriseTrial = isTrial && isEnterprise(planDetails.id); const reservedBudgets = []; - if (hasSeer) { + if (hasLegacySeer) { if (isTrial) { reservedBudgets.push(SeerReservedBudgetFixture({reservedBudget: 150_00})); } else { @@ -240,7 +240,7 @@ export function SubscriptionFixture(props: Props): TSubscription { order: 11, }), }), - ...(hasSeer && { + ...(hasLegacySeer && { seerAutofix: MetricHistoryFixture({ category: DataCategory.SEER_AUTOFIX, reserved: 0, @@ -262,9 +262,9 @@ export function SubscriptionFixture(props: Props): TSubscription { /** * Returns a subscription with self-serve paid Seer reserved budget. */ -export function SubscriptionWithSeerFixture(props: Props): TSubscription { +export function SubscriptionWithLegacySeerFixture(props: Props): TSubscription { const subscription = SubscriptionFixture(props); - if (!subscription.planDetails.addOnCategories[AddOnCategory.SEER]) { + if (!subscription.planDetails.addOnCategories[AddOnCategory.LEGACY_SEER]) { return subscription; } @@ -286,9 +286,9 @@ export function SubscriptionWithSeerFixture(props: Props): TSubscription { subscription.reservedBudgets = [SeerReservedBudgetFixture({})]; subscription.addOns = { ...subscription.addOns, - [AddOnCategory.SEER]: { - ...(subscription.addOns?.[AddOnCategory.SEER] ?? - subscription.planDetails.addOnCategories[AddOnCategory.SEER]), + [AddOnCategory.LEGACY_SEER]: { + ...(subscription.addOns?.[AddOnCategory.LEGACY_SEER] ?? + subscription.planDetails.addOnCategories[AddOnCategory.LEGACY_SEER]), enabled: true, }, };