From 4c5c4f0b08b27418abed66db10cfd49ba332b3a3 Mon Sep 17 00:00:00 2001 From: isabellaenriquez Date: Thu, 20 Nov 2025 10:47:41 -0500 Subject: [PATCH 1/4] ref(seer): Introduce legacy add-on --- static/gsApp/types/index.tsx | 1 + .../views/amCheckout/steps/productSelect.tsx | 52 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index 5a22e78ab6e53d..5ffe767356ba27 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -137,6 +137,7 @@ export type ReservedBudgetCategory = { export enum AddOnCategory { SEER = 'seer', PREVENT = 'prevent', + LEGACY_SEER = 'legacySeer', } export type AddOnCategoryInfo = { diff --git a/static/gsApp/views/amCheckout/steps/productSelect.tsx b/static/gsApp/views/amCheckout/steps/productSelect.tsx index be93765b6ece7e..a4b4293f67f23e 100644 --- a/static/gsApp/views/amCheckout/steps/productSelect.tsx +++ b/static/gsApp/views/amCheckout/steps/productSelect.tsx @@ -96,33 +96,35 @@ function ProductSelect({ ); const theme = useTheme(); - const PRODUCT_CHECKOUT_INFO = { - [AddOnCategory.SEER]: { - 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 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({ From fe7a65f84fa1195e1a800fbb121dfb81ba105e2a Mon Sep 17 00:00:00 2001 From: isabellaenriquez Date: Thu, 20 Nov 2025 10:56:06 -0500 Subject: [PATCH 2/4] include this too --- .../components/changePlanAction.spec.tsx | 25 +++++----------- .../gsAdmin/components/changePlanAction.tsx | 30 ++----------------- static/gsAdmin/components/planList.tsx | 10 +++++-- 3 files changed, 19 insertions(+), 46 deletions(-) diff --git a/static/gsAdmin/components/changePlanAction.spec.tsx b/static/gsAdmin/components/changePlanAction.spec.tsx index 288dea2e837f8f..1c06f52dfea5c8 100644 --- a/static/gsAdmin/components/changePlanAction.spec.tsx +++ b/static/gsAdmin/components/changePlanAction.spec.tsx @@ -4,8 +4,10 @@ import {UserFixture} from 'sentry-fixture/user'; import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig'; import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory'; import {PlanDetailsLookupFixture} from 'getsentry-test/fixtures/planDetailsLookup'; -import {SeerReservedBudgetFixture} from 'getsentry-test/fixtures/reservedBudget'; -import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription'; +import { + SubscriptionFixture, + SubscriptionWithSeerFixture, +} from 'getsentry-test/fixtures/subscription'; import { renderGlobalModal, screen, @@ -239,7 +241,7 @@ describe('ChangePlanAction', () => { }); it('completes form with addOns', async () => { - mockOrg.features = ['seer-billing', 'seer-user-billing']; + mockOrg.features = ['seer-billing', 'seer-user-billing']; // this won't happen IRL, but doing this for testing multiple addons const putMock = MockApiClient.addMockResponse({ url: `/customers/${mockOrg.slug}/subscription/`, method: 'PUT', @@ -378,7 +380,7 @@ describe('ChangePlanAction', () => { expect(requestData).toHaveProperty('reservedTransactions', 25000); }); - describe('Seer Budget', () => { + describe('Legacy Seer', () => { beforeEach(() => { mockOrg.features = ['seer-billing']; jest.clearAllMocks(); @@ -445,21 +447,10 @@ describe('ChangePlanAction', () => { it('initializes Seer budget checkbox based on current subscription', async () => { // Create subscription with Seer budget - const subscriptionWithSeer = SubscriptionFixture({ + const subscriptionWithSeer = SubscriptionWithSeerFixture({ organization: mockOrg, planTier: PlanTier.AM3, plan: 'am3_business', - billingInterval: 'monthly', - contractInterval: 'monthly', - reservedBudgets: [SeerReservedBudgetFixture({})], - categories: { - errors: MetricHistoryFixture({ - category: DataCategory.ERRORS, - reserved: 1000000, - prepaid: 1000000, - order: 1, - }), - }, }); SubscriptionStore.set(mockOrg.slug, subscriptionWithSeer); @@ -555,7 +546,7 @@ describe('ChangePlanAction', () => { expect(requestData).toHaveProperty('addOnSeer', true); }); - it('does not include seer parameter in form submission when checkbox is unchecked', async () => { + it('does not include add-on parameter in form submission when checkbox is unchecked', async () => { // Mock the PUT endpoint response const putMock = MockApiClient.addMockResponse({ url: `/customers/${mockOrg.slug}/subscription/`, diff --git a/static/gsAdmin/components/changePlanAction.tsx b/static/gsAdmin/components/changePlanAction.tsx index 74ed2a66fde8cd..fe19630c459eb0 100644 --- a/static/gsAdmin/components/changePlanAction.tsx +++ b/static/gsAdmin/components/changePlanAction.tsx @@ -24,7 +24,7 @@ import useApi from 'sentry/utils/useApi'; import PlanList from 'admin/components/planList'; import {ANNUAL, MONTHLY} from 'getsentry/constants'; import type {BillingConfig, Plan, Subscription} from 'getsentry/types'; -import {CheckoutType, PlanTier, ReservedBudgetCategoryType} from 'getsentry/types'; +import {CheckoutType, PlanTier} from 'getsentry/types'; const ALLOWED_TIERS = [PlanTier.MM2, PlanTier.AM1, PlanTier.AM2, PlanTier.AM3]; @@ -49,23 +49,6 @@ function ChangePlanAction({ const [formModel] = useState(() => new FormModel()); const orgId = organization.slug; - /** - * Check if the current subscription has Seer budget enabled - */ - const hasCurrentSeerBudget = useMemo(() => { - return ( - subscription.reservedBudgets?.some( - budget => - budget.apiName === ReservedBudgetCategoryType.SEER && budget.reservedBudget > 0 - ) ?? false - ); - }, [subscription.reservedBudgets]); - - // Initialize Seer budget value in form model - React.useEffect(() => { - formModel.setValue('addOnSeer', hasCurrentSeerBudget); - }, [formModel, hasCurrentSeerBudget]); - const api = useApi({persistInFlight: true}); const { data: configs, @@ -214,17 +197,11 @@ function ChangePlanAction({ return; } - // Add Seer budget parameter for AM plans and TEST tier - const submitData = { - ...data, - addOnSeer: formModel.getValue('addOnSeer'), - }; - if (activeTier === PlanTier.MM2) { try { await api.requestPromise(`/customers/${orgId}/`, { method: 'PUT', - data: submitData, + data, }); onSubmitSuccess(data); } catch (error) { @@ -237,7 +214,7 @@ function ChangePlanAction({ try { await api.requestPromise(`/customers/${orgId}/subscription/`, { method: 'PUT', - data: submitData, + data, }); onSubmitSuccess(data); onSuccess?.(); @@ -281,7 +258,6 @@ function ChangePlanAction({ setActiveTier(tab); setBillingInterval(MONTHLY); setContractInterval(MONTHLY); - formModel.setValue('addOnSeer', hasCurrentSeerBudget); }} > diff --git a/static/gsAdmin/components/planList.tsx b/static/gsAdmin/components/planList.tsx index e21c719f3442e9..ed6203fa072416 100644 --- a/static/gsAdmin/components/planList.tsx +++ b/static/gsAdmin/components/planList.tsx @@ -87,6 +87,12 @@ function PlanList({ return productInfo; }); + availableProducts.forEach(productInfo => { + const addOnKey = `addOn${toTitleCase(productInfo.apiName, {allowInnerUpperCase: true})}`; + const enabled = subscription.addOns?.[productInfo.apiName]?.enabled; + formModel.setValue(addOnKey, enabled); + }); + return (

Available Products

{availableProducts.map(productInfo => { - const addOnKey = `addOn${toTitleCase(productInfo.apiName)}`; + const addOnKey = `addOn${toTitleCase(productInfo.apiName, {allowInnerUpperCase: true})}`; return ( Date: Thu, 20 Nov 2025 11:02:18 -0500 Subject: [PATCH 3/4] make ts happy --- tests/js/getsentry-test/fixtures/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js/getsentry-test/fixtures/constants.ts b/tests/js/getsentry-test/fixtures/constants.ts index eecf96949456e6..f3e196111f23d1 100644 --- a/tests/js/getsentry-test/fixtures/constants.ts +++ b/tests/js/getsentry-test/fixtures/constants.ts @@ -21,4 +21,4 @@ export const AM_ADD_ON_CATEGORIES = { order: 2, productName: 'prevent', }, -} satisfies Record; +} 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 From 6f8f99686bad9faba828640443a563f8031e7761 Mon Sep 17 00:00:00 2001 From: isabellaenriquez Date: Thu, 20 Nov 2025 11:25:05 -0500 Subject: [PATCH 4/4] fine put it back --- static/gsAdmin/components/planList.tsx | 34 ++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/static/gsAdmin/components/planList.tsx b/static/gsAdmin/components/planList.tsx index ed6203fa072416..3bdf4a79a59b3a 100644 --- a/static/gsAdmin/components/planList.tsx +++ b/static/gsAdmin/components/planList.tsx @@ -1,3 +1,4 @@ +import {useEffect, useMemo} from 'react'; import styled from '@emotion/styled'; import CheckboxField from 'sentry/components/forms/fields/checkboxField'; @@ -78,20 +79,27 @@ function PlanList({ 100000: '100K', }; - const availableProducts = Object.values(activePlan?.addOnCategories || {}) - .filter( - productInfo => - productInfo.billingFlag && organization.features.includes(productInfo.billingFlag) - ) - .map(productInfo => { - return productInfo; - }); + const availableProducts = useMemo( + () => + Object.values(activePlan?.addOnCategories || {}) + .filter( + productInfo => + productInfo.billingFlag && + organization.features.includes(productInfo.billingFlag) + ) + .map(productInfo => { + return productInfo; + }), + [activePlan?.addOnCategories, organization.features] + ); - availableProducts.forEach(productInfo => { - const addOnKey = `addOn${toTitleCase(productInfo.apiName, {allowInnerUpperCase: true})}`; - const enabled = subscription.addOns?.[productInfo.apiName]?.enabled; - formModel.setValue(addOnKey, enabled); - }); + useEffect(() => { + availableProducts.forEach(productInfo => { + const addOnKey = `addOn${toTitleCase(productInfo.apiName, {allowInnerUpperCase: true})}`; + const enabled = subscription.addOns?.[productInfo.apiName]?.enabled; + formModel.setValue(addOnKey, enabled); + }); + }, [availableProducts, subscription.addOns, formModel]); return (