diff --git a/static/gsAdmin/components/addGiftEventsAction.spec.tsx b/static/gsAdmin/components/addGiftEventsAction.spec.tsx index 75958275b543..522eee186d42 100644 --- a/static/gsAdmin/components/addGiftEventsAction.spec.tsx +++ b/static/gsAdmin/components/addGiftEventsAction.spec.tsx @@ -7,8 +7,10 @@ import {DataCategory, DataCategoryExact} from 'sentry/types/core'; import {AddGiftEventsAction} from 'admin/components/addGiftEventsAction'; import {openAdminConfirmModal} from 'admin/components/adminConfirmationModal'; -import {BILLED_DATA_CATEGORY_INFO} from 'getsentry/constants'; -import {getCategoryInfoFromPlural} from 'getsentry/utils/dataCategory'; +import { + getBilledDataCategoryInfo, + getCategoryInfoFromPlural, +} from 'getsentry/utils/dataCategory'; describe('AddGiftEventsAction', () => { const mockOrg = OrganizationFixture(); @@ -18,7 +20,7 @@ describe('AddGiftEventsAction', () => { const triggerGiftModal = () => { const billedCategoryInfo = categoryOverride ? getCategoryInfoFromPlural(categoryOverride) - : BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SPAN]; + : getBilledDataCategoryInfo()[DataCategoryExact.SPAN]; openAdminConfirmModal({ renderModalSpecificContent: deps => ( { // Category must be in the plan's categories if (!customer.planDetails?.categories.includes(categoryInfo.plural)) { diff --git a/static/gsApp/components/gsBanner.tsx b/static/gsApp/components/gsBanner.tsx index 5fedcb7be4bb..420dd9beb43f 100644 --- a/static/gsApp/components/gsBanner.tsx +++ b/static/gsApp/components/gsBanner.tsx @@ -44,7 +44,6 @@ import {ProductTrialAlert} from 'getsentry/components/productTrial/productTrialA import {getProductForPath} from 'getsentry/components/productTrial/productTrialPaths'; import {makeLinkToOwnersAndBillingMembers} from 'getsentry/components/profiling/alerts'; import {withSubscription} from 'getsentry/components/withSubscription'; -import {BILLED_DATA_CATEGORY_INFO} from 'getsentry/constants'; import {SubscriptionStore} from 'getsentry/stores/subscriptionStore'; import { type BilledDataCategoryInfo, @@ -62,7 +61,10 @@ import { partnerPlanEndingModalIsDismissed, trialPromptIsDismissed, } from 'getsentry/utils/billing'; -import {getCategoryInfoFromPlural} from 'getsentry/utils/dataCategory'; +import { + getBilledDataCategoryInfo, + getCategoryInfoFromPlural, +} from 'getsentry/utils/dataCategory'; import {getPendoAccountFields} from 'getsentry/utils/pendo'; import {claimAvailablePromotion} from 'getsentry/utils/promotionUtils'; import {trackGetsentryAnalytics} from 'getsentry/utils/trackGetsentryAnalytics'; @@ -80,7 +82,7 @@ enum ModalType { const TRIAL_ENDING_DAY_WINDOW = 3; function objectFromBilledCategories(callback: (c: BilledDataCategoryInfo) => any) { - return Object.values(BILLED_DATA_CATEGORY_INFO).reduce( + return Object.values(getBilledDataCategoryInfo()).reduce( (acc, c) => { if (c.isBilledCategory) { acc[c.singular as EventType] = callback(c); @@ -634,7 +636,7 @@ class GSBanner extends Component { const category_warning_prompts: string[] = []; const category_product_trial_prompts: string[] = []; - Object.values(BILLED_DATA_CATEGORY_INFO) + Object.values(getBilledDataCategoryInfo()) .filter( categoryInfo => categoryInfo.isBilledCategory && diff --git a/static/gsApp/constants.tsx b/static/gsApp/constants.tsx index 76cf0c1174a8..e6bddc4f6648 100644 --- a/static/gsApp/constants.tsx +++ b/static/gsApp/constants.tsx @@ -1,9 +1,3 @@ -import {DATA_CATEGORY_INFO} from 'sentry/constants'; -import {t} from 'sentry/locale'; -import {DataCategoryExact} from 'sentry/types/core'; - -import type {BilledDataCategoryInfo} from 'getsentry/types'; - export const MONTHLY = 'monthly'; export const ANNUAL = 'annual'; @@ -48,189 +42,3 @@ export enum AllocationTargetTypes { PROJECT = 'Project', ORGANIZATION = 'Organization', } - -// XXX: initialize the BilledDataCategoryInfo-specific field for all non-billed -// `categories and make TS happy so we can access the BilledDataCategoryInfo -// fields directly without needing to check that they exist on the object -const DEFAULT_BILLED_DATA_CATEGORY_INFO = { - ...DATA_CATEGORY_INFO, -} as Record; -Object.entries(DEFAULT_BILLED_DATA_CATEGORY_INFO).forEach( - ([categoryExact, categoryInfo]) => { - DEFAULT_BILLED_DATA_CATEGORY_INFO[categoryExact as DataCategoryExact] = { - ...categoryInfo, - canAllocate: false, - canProductTrial: false, - freeEventsMultiple: 0, - feature: null, - hasSpikeProtection: false, - checkoutTooltip: null, - tallyType: 'usage', - hasPerCategory: false, - adminOnlyProductTrialFeature: null, - }; - } -); - -/** - * Extension of DATA_CATEGORY_INFO with billing info for billed categories. - * All categories with isBilledCategory: true, should be explicitly - * added to this object with billing info. - */ -export const BILLED_DATA_CATEGORY_INFO = { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO, - [DataCategoryExact.ERROR]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.ERROR], - canAllocate: true, - freeEventsMultiple: 1_000, - hasSpikeProtection: true, - checkoutTooltip: t( - 'Errors are sent every time an SDK catches a bug. You can send them manually too, if you want.' - ), - hasPerCategory: true, - }, - [DataCategoryExact.TRANSACTION]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.TRANSACTION], - canAllocate: true, - canProductTrial: true, - freeEventsMultiple: 1_000, - feature: 'performance-view', - hasSpikeProtection: true, - checkoutTooltip: t( - 'Transactions are sent when your service receives a request and sends a response.' - ), - hasPerCategory: true, - shortenedUnitName: t('unit'), - }, - [DataCategoryExact.ATTACHMENT]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.ATTACHMENT], - canAllocate: true, - freeEventsMultiple: 1, - feature: 'event-attachments', - hasSpikeProtection: true, - checkoutTooltip: t('Attachments are files attached to errors, such as minidumps.'), - hasPerCategory: true, - shortenedUnitName: 'GB', - }, - [DataCategoryExact.REPLAY]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.REPLAY], - canProductTrial: true, - freeEventsMultiple: 1, - feature: 'session-replay', - checkoutTooltip: t( - 'Session Replays are video-like reproductions of your users’ sessions navigating your app or website.' - ), - hasPerCategory: true, - }, - [DataCategoryExact.SPAN]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SPAN], - canProductTrial: true, - freeEventsMultiple: 100_000, - feature: 'spans-usage-tracking', - hasSpikeProtection: true, - checkoutTooltip: t( - 'Tracing is enabled by spans. A span represents a single operation of work within a trace.' - ), - }, - [DataCategoryExact.SPAN_INDEXED]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SPAN_INDEXED], - canProductTrial: true, - freeEventsMultiple: 100_000, - feature: 'spans-usage-tracking', - }, - [DataCategoryExact.MONITOR_SEAT]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.MONITOR_SEAT], - freeEventsMultiple: 1, - feature: 'monitor-seat-billing', - tallyType: 'seat', - hasPerCategory: true, - checkoutTooltip: t( - 'Crons monitors scheduled jobs to confirm they run on time and alert you when they fail or misfire.' - ), - shortenedUnitName: t('monitor'), - }, - [DataCategoryExact.UPTIME]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.UPTIME], - freeEventsMultiple: 1, - feature: 'uptime-billing', - tallyType: 'seat', - hasPerCategory: true, - checkoutTooltip: t( - 'Uptime monitoring checks your application’s availability and alerts you when services go down so you can respond quickly.' - ), - shortenedUnitName: t('monitor'), - }, - [DataCategoryExact.PROFILE_DURATION]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.PROFILE_DURATION], - canProductTrial: true, - freeEventsMultiple: 1, // in hours - hasPerCategory: true, - checkoutTooltip: t( - 'Continuous profiling tracks how code runs while your service is active, helping you find bottlenecks and improve efficiency.' - ), - shortenedUnitName: t('hour'), - }, - [DataCategoryExact.PROFILE_DURATION_UI]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.PROFILE_DURATION_UI], - canProductTrial: true, - freeEventsMultiple: 1, // in hours - hasPerCategory: true, - checkoutTooltip: t( - 'UI profiling tracks code performance during user sessions in frontend or mobile apps, helping you spot slowdowns and improve experience.' - ), - shortenedUnitName: t('hour'), - }, - // Seer categories have product trials through ReservedBudgetCategoryType.SEER, not as individual categories - [DataCategoryExact.SEER_AUTOFIX]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_AUTOFIX], - feature: 'seer-billing', - shortenedUnitName: t('fix'), - }, - [DataCategoryExact.SEER_SCANNER]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_SCANNER], - feature: 'seer-billing', - shortenedUnitName: t('scan'), - }, - [DataCategoryExact.LOG_BYTE]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.LOG_BYTE], - canAllocate: false, - canProductTrial: true, - freeEventsMultiple: 1, - hasSpikeProtection: false, - feature: 'logs-billing', - checkoutTooltip: t( - 'A log records events from your application, giving you the context to debug issues and understand system behavior.' - ), - shortenedUnitName: 'GB', - }, - [DataCategoryExact.TRACE_METRIC_BYTE]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.TRACE_METRIC_BYTE], - canProductTrial: true, - freeEventsMultiple: 1, - feature: 'expose-category-trace-metric-byte', - shortenedUnitName: 'GB', - checkoutTooltip: t( - 'Application Metrics capture key signals from your application using counters, gauges, and distributions.' - ), - }, - [DataCategoryExact.SEER_USER]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_USER], - feature: 'seer-user-billing-launch', - canProductTrial: false, - freeEventsMultiple: 1, - tallyType: 'seat', - shortenedUnitName: t('contributor'), - }, - [DataCategoryExact.SIZE_ANALYSIS]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SIZE_ANALYSIS], - freeEventsMultiple: 1, - shortenedUnitName: t('build'), - adminOnlyProductTrialFeature: true, - }, - [DataCategoryExact.INSTALLABLE_BUILD]: { - ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.INSTALLABLE_BUILD], - freeEventsMultiple: 1, - shortenedUnitName: t('install'), - adminOnlyProductTrialFeature: 'expose-category-installable-build', - }, -} as const satisfies Record; diff --git a/static/gsApp/utils/dataCategory.tsx b/static/gsApp/utils/dataCategory.tsx index 157f8b8b4219..75cd71e3fca9 100644 --- a/static/gsApp/utils/dataCategory.tsx +++ b/static/gsApp/utils/dataCategory.tsx @@ -6,7 +6,7 @@ import {DataCategory, DataCategoryExact} from 'sentry/types/core'; import {oxfordizeArray} from 'sentry/utils/oxfordizeArray'; import {toTitleCase} from 'sentry/utils/string/toTitleCase'; -import {BILLED_DATA_CATEGORY_INFO, UNLIMITED_RESERVED} from 'getsentry/constants'; +import {UNLIMITED_RESERVED} from 'getsentry/constants'; import type { BilledDataCategoryInfo, BillingMetricHistory, @@ -19,6 +19,203 @@ import type { } from 'getsentry/types'; import {MILLISECONDS_IN_HOUR} from 'getsentry/utils/billing'; +// XXX: initialize the BilledDataCategoryInfo-specific field for all non-billed +// `categories and make TS happy so we can access the BilledDataCategoryInfo +// fields directly without needing to check that they exist on the object +const DEFAULT_BILLED_DATA_CATEGORY_INFO = { + ...DATA_CATEGORY_INFO, +} as Record; +Object.entries(DEFAULT_BILLED_DATA_CATEGORY_INFO).forEach( + ([categoryExact, categoryInfo]) => { + DEFAULT_BILLED_DATA_CATEGORY_INFO[categoryExact as DataCategoryExact] = { + ...categoryInfo, + canAllocate: false, + canProductTrial: false, + freeEventsMultiple: 0, + feature: null, + hasSpikeProtection: false, + checkoutTooltip: null, + tallyType: 'usage', + hasPerCategory: false, + adminOnlyProductTrialFeature: null, + }; + } +); + +/** + * Extension of DATA_CATEGORY_INFO with billing info for billed categories. + * All categories with isBilledCategory: true, should be explicitly + * added to this object with billing info. + */ +const BILLED_DATA_CATEGORY_INFO = { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO, + [DataCategoryExact.ERROR]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.ERROR], + canAllocate: true, + freeEventsMultiple: 1_000, + hasSpikeProtection: true, + checkoutTooltip: t( + 'Errors are sent every time an SDK catches a bug. You can send them manually too, if you want.' + ), + hasPerCategory: true, + }, + [DataCategoryExact.TRANSACTION]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.TRANSACTION], + canAllocate: true, + canProductTrial: true, + freeEventsMultiple: 1_000, + feature: 'performance-view', + hasSpikeProtection: true, + checkoutTooltip: t( + 'Transactions are sent when your service receives a request and sends a response.' + ), + hasPerCategory: true, + shortenedUnitName: t('unit'), + }, + [DataCategoryExact.ATTACHMENT]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.ATTACHMENT], + canAllocate: true, + freeEventsMultiple: 1, + feature: 'event-attachments', + hasSpikeProtection: true, + checkoutTooltip: t('Attachments are files attached to errors, such as minidumps.'), + hasPerCategory: true, + shortenedUnitName: 'GB', + }, + [DataCategoryExact.REPLAY]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.REPLAY], + canProductTrial: true, + freeEventsMultiple: 1, + feature: 'session-replay', + checkoutTooltip: t( + 'Session Replays are video-like reproductions of your users\u2019 sessions navigating your app or website.' + ), + hasPerCategory: true, + }, + [DataCategoryExact.SPAN]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SPAN], + canProductTrial: true, + freeEventsMultiple: 100_000, + feature: 'spans-usage-tracking', + hasSpikeProtection: true, + checkoutTooltip: t( + 'Tracing is enabled by spans. A span represents a single operation of work within a trace.' + ), + }, + [DataCategoryExact.SPAN_INDEXED]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SPAN_INDEXED], + canProductTrial: true, + freeEventsMultiple: 100_000, + feature: 'spans-usage-tracking', + }, + [DataCategoryExact.MONITOR_SEAT]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.MONITOR_SEAT], + freeEventsMultiple: 1, + feature: 'monitor-seat-billing', + tallyType: 'seat', + hasPerCategory: true, + checkoutTooltip: t( + 'Crons monitors scheduled jobs to confirm they run on time and alert you when they fail or misfire.' + ), + shortenedUnitName: t('monitor'), + }, + [DataCategoryExact.UPTIME]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.UPTIME], + freeEventsMultiple: 1, + feature: 'uptime-billing', + tallyType: 'seat', + hasPerCategory: true, + checkoutTooltip: t( + 'Uptime monitoring checks your application\u2019s availability and alerts you when services go down so you can respond quickly.' + ), + shortenedUnitName: t('monitor'), + }, + [DataCategoryExact.PROFILE_DURATION]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.PROFILE_DURATION], + canProductTrial: true, + freeEventsMultiple: 1, // in hours + hasPerCategory: true, + checkoutTooltip: t( + 'Continuous profiling tracks how code runs while your service is active, helping you find bottlenecks and improve efficiency.' + ), + shortenedUnitName: t('hour'), + }, + [DataCategoryExact.PROFILE_DURATION_UI]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.PROFILE_DURATION_UI], + canProductTrial: true, + freeEventsMultiple: 1, // in hours + hasPerCategory: true, + checkoutTooltip: t( + 'UI profiling tracks code performance during user sessions in frontend or mobile apps, helping you spot slowdowns and improve experience.' + ), + shortenedUnitName: t('hour'), + }, + // Seer categories have product trials through ReservedBudgetCategoryType.SEER, not as individual categories + [DataCategoryExact.SEER_AUTOFIX]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_AUTOFIX], + feature: 'seer-billing', + shortenedUnitName: t('fix'), + }, + [DataCategoryExact.SEER_SCANNER]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_SCANNER], + feature: 'seer-billing', + shortenedUnitName: t('scan'), + }, + [DataCategoryExact.LOG_BYTE]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.LOG_BYTE], + canAllocate: false, + canProductTrial: true, + freeEventsMultiple: 1, + hasSpikeProtection: false, + feature: 'logs-billing', + checkoutTooltip: t( + 'A log records events from your application, giving you the context to debug issues and understand system behavior.' + ), + shortenedUnitName: 'GB', + }, + [DataCategoryExact.TRACE_METRIC_BYTE]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.TRACE_METRIC_BYTE], + canProductTrial: true, + freeEventsMultiple: 1, + feature: 'expose-category-trace-metric-byte', + shortenedUnitName: 'GB', + checkoutTooltip: t( + 'Application Metrics capture key signals from your application using counters, gauges, and distributions.' + ), + }, + [DataCategoryExact.SEER_USER]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_USER], + feature: 'seer-user-billing-launch', + canProductTrial: false, + freeEventsMultiple: 1, + tallyType: 'seat', + shortenedUnitName: t('contributor'), + }, + [DataCategoryExact.SIZE_ANALYSIS]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SIZE_ANALYSIS], + freeEventsMultiple: 1, + shortenedUnitName: t('build'), + adminOnlyProductTrialFeature: true, + }, + [DataCategoryExact.INSTALLABLE_BUILD]: { + ...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.INSTALLABLE_BUILD], + freeEventsMultiple: 1, + shortenedUnitName: t('install'), + adminOnlyProductTrialFeature: 'expose-category-installable-build', + }, +} as const satisfies Record; + +/** + * Returns billing-enriched data category info for all categories. + * This wraps the static constant so it can later be replaced with a backend call. + */ +export function getBilledDataCategoryInfo(): Record< + DataCategoryExact, + BilledDataCategoryInfo +> { + return BILLED_DATA_CATEGORY_INFO; +} + /** * Returns the data category info defined in DATA_CATEGORY_INFO for the given category, * with billing context defined in BILLED_DATA_CATEGORY_INFO. diff --git a/static/gsApp/views/spendAllocations/components/allocationForm.tsx b/static/gsApp/views/spendAllocations/components/allocationForm.tsx index 03b5cb254366..f20748247ac7 100644 --- a/static/gsApp/views/spendAllocations/components/allocationForm.tsx +++ b/static/gsApp/views/spendAllocations/components/allocationForm.tsx @@ -20,9 +20,10 @@ import type {RequestMethod} from 'sentry/utils/api/apiQueryKey'; import {useApi} from 'sentry/utils/useApi'; import {useOrganization} from 'sentry/utils/useOrganization'; -import {AllocationTargetTypes, BILLED_DATA_CATEGORY_INFO} from 'getsentry/constants'; +import {AllocationTargetTypes} from 'getsentry/constants'; import type {Subscription} from 'getsentry/types'; import { + getBilledDataCategoryInfo, getCategoryInfoFromPlural, getPlanCategoryName, } from 'getsentry/utils/dataCategory'; @@ -541,10 +542,10 @@ const Select = styled(SelectField)` } `; -// Normalizes singular billingMetric values to match DataCategory enum using BILLED_DATA_CATEGORY_INFO +// Normalizes singular billingMetric values to match DataCategory enum using getBilledDataCategoryInfo() function normalizeBillingMetric(metric: string): DataCategory { return ( - Object.values(BILLED_DATA_CATEGORY_INFO) + Object.values(getBilledDataCategoryInfo()) .filter(info => info.canAllocate) .find(c => c.name === metric)?.plural ?? (metric as DataCategory) );